mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
640 lines
14 KiB
640 lines
14 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: BUSL-1.1 |
|
|
|
package checks |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"reflect" |
|
"sync/atomic" |
|
"testing" |
|
"time" |
|
|
|
"github.com/hashicorp/consul/acl" |
|
"github.com/hashicorp/consul/agent/mock" |
|
"github.com/hashicorp/consul/agent/structs" |
|
"github.com/hashicorp/consul/api" |
|
"github.com/hashicorp/consul/sdk/testutil/retry" |
|
"github.com/hashicorp/consul/types" |
|
//"github.com/stretchr/testify/require" |
|
) |
|
|
|
// Test that we do a backoff on error. |
|
func TestCheckAlias_remoteErrBackoff(t *testing.T) { |
|
if testing.Short() { |
|
t.Skip("too slow for testing.Short") |
|
} |
|
|
|
t.Parallel() |
|
|
|
notify := newMockAliasNotify() |
|
chkID := structs.NewCheckID(types.CheckID("foo"), nil) |
|
rpc := &mockRPC{} |
|
chk := &CheckAlias{ |
|
Node: "remote", |
|
ServiceID: structs.ServiceID{ID: "web"}, |
|
CheckID: chkID, |
|
Notify: notify, |
|
RPC: rpc, |
|
} |
|
|
|
rpc.AddReply("Health.NodeChecks", fmt.Errorf("failure")) |
|
|
|
chk.Start() |
|
defer chk.Stop() |
|
|
|
time.Sleep(100 * time.Millisecond) |
|
if got, want := atomic.LoadUint32(&rpc.Calls), uint32(6); got > want { |
|
t.Fatalf("got %d updates want at most %d", got, want) |
|
} |
|
|
|
retry.Run(t, func(r *retry.R) { |
|
if got, want := notify.State(chkID), api.HealthCritical; got != want { |
|
r.Fatalf("got state %q want %q", got, want) |
|
} |
|
}) |
|
} |
|
|
|
// No remote health checks should result in passing on the check. |
|
func TestCheckAlias_remoteNoChecks(t *testing.T) { |
|
if testing.Short() { |
|
t.Skip("too slow for testing.Short") |
|
} |
|
|
|
t.Parallel() |
|
|
|
notify := newMockAliasNotify() |
|
chkID := structs.NewCheckID(types.CheckID("foo"), nil) |
|
rpc := &mockRPC{} |
|
chk := &CheckAlias{ |
|
Node: "remote", |
|
ServiceID: structs.ServiceID{ID: "web"}, |
|
CheckID: chkID, |
|
Notify: notify, |
|
RPC: rpc, |
|
} |
|
|
|
rpc.AddReply("Health.NodeChecks", structs.IndexedHealthChecks{}) |
|
|
|
chk.Start() |
|
defer chk.Stop() |
|
retry.Run(t, func(r *retry.R) { |
|
if got, want := notify.State(chkID), api.HealthPassing; got != want { |
|
r.Fatalf("got state %q want %q", got, want) |
|
} |
|
}) |
|
} |
|
|
|
// If the node is critical then the check is critical |
|
func TestCheckAlias_remoteNodeFailure(t *testing.T) { |
|
if testing.Short() { |
|
t.Skip("too slow for testing.Short") |
|
} |
|
|
|
t.Parallel() |
|
|
|
notify := newMockAliasNotify() |
|
chkID := structs.NewCheckID(types.CheckID("foo"), nil) |
|
rpc := &mockRPC{} |
|
chk := &CheckAlias{ |
|
Node: "remote", |
|
ServiceID: structs.ServiceID{ID: "web"}, |
|
CheckID: chkID, |
|
Notify: notify, |
|
RPC: rpc, |
|
} |
|
|
|
rpc.AddReply("Health.NodeChecks", structs.IndexedHealthChecks{ |
|
HealthChecks: []*structs.HealthCheck{ |
|
// Should ignore non-matching node |
|
{ |
|
Node: "A", |
|
ServiceID: "web", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Node failure |
|
{ |
|
Node: "remote", |
|
ServiceID: "", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Match |
|
{ |
|
Node: "remote", |
|
ServiceID: "web", |
|
Status: api.HealthPassing, |
|
}, |
|
}, |
|
}) |
|
|
|
chk.Start() |
|
defer chk.Stop() |
|
retry.Run(t, func(r *retry.R) { |
|
if got, want := notify.State(chkID), api.HealthCritical; got != want { |
|
r.Fatalf("got state %q want %q", got, want) |
|
} |
|
}) |
|
} |
|
|
|
// Only passing should result in passing |
|
func TestCheckAlias_remotePassing(t *testing.T) { |
|
t.Parallel() |
|
|
|
notify := newMockAliasNotify() |
|
chkID := structs.NewCheckID("foo", nil) |
|
rpc := &mockRPC{} |
|
chk := &CheckAlias{ |
|
Node: "remote", |
|
ServiceID: structs.ServiceID{ID: "web"}, |
|
CheckID: chkID, |
|
Notify: notify, |
|
RPC: rpc, |
|
} |
|
|
|
rpc.AddReply("Health.NodeChecks", structs.IndexedHealthChecks{ |
|
HealthChecks: []*structs.HealthCheck{ |
|
// Should ignore non-matching node |
|
{ |
|
Node: "A", |
|
ServiceID: "web", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Should ignore non-matching service |
|
{ |
|
Node: "remote", |
|
ServiceID: "db", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Match |
|
{ |
|
Node: "remote", |
|
ServiceID: "web", |
|
Status: api.HealthPassing, |
|
}, |
|
}, |
|
}) |
|
|
|
chk.Start() |
|
defer chk.Stop() |
|
retry.Run(t, func(r *retry.R) { |
|
if got, want := notify.State(chkID), api.HealthPassing; got != want { |
|
r.Fatalf("got state %q want %q", got, want) |
|
} |
|
}) |
|
} |
|
|
|
// Remote service has no healtchecks, but service exists on remote host |
|
func TestCheckAlias_remotePassingWithoutChecksButWithService(t *testing.T) { |
|
t.Parallel() |
|
|
|
notify := newMockAliasNotify() |
|
chkID := structs.NewCheckID("foo", nil) |
|
rpc := &mockRPC{} |
|
chk := &CheckAlias{ |
|
Node: "remote", |
|
ServiceID: structs.ServiceID{ID: "web"}, |
|
CheckID: chkID, |
|
Notify: notify, |
|
RPC: rpc, |
|
} |
|
|
|
rpc.AddReply("Health.NodeChecks", structs.IndexedHealthChecks{ |
|
HealthChecks: []*structs.HealthCheck{ |
|
// Should ignore non-matching node |
|
{ |
|
Node: "A", |
|
ServiceID: "web", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Should ignore non-matching service |
|
{ |
|
Node: "remote", |
|
ServiceID: "db", |
|
Status: api.HealthCritical, |
|
}, |
|
}, |
|
}) |
|
|
|
injected := structs.IndexedNodeServices{ |
|
NodeServices: &structs.NodeServices{ |
|
Node: &structs.Node{ |
|
Node: "remote", |
|
}, |
|
Services: make(map[string]*structs.NodeService), |
|
}, |
|
QueryMeta: structs.QueryMeta{}, |
|
} |
|
injected.NodeServices.Services["web"] = &structs.NodeService{ |
|
Service: "web", |
|
ID: "web", |
|
} |
|
rpc.AddReply("Catalog.NodeServices", injected) |
|
|
|
chk.Start() |
|
defer chk.Stop() |
|
retry.Run(t, func(r *retry.R) { |
|
if got, want := notify.State(chkID), api.HealthPassing; got != want { |
|
r.Fatalf("got state %q want %q", got, want) |
|
} |
|
}) |
|
} |
|
|
|
// Remote service has no healtchecks, service does not exists on remote host |
|
func TestCheckAlias_remotePassingWithoutChecksAndWithoutService(t *testing.T) { |
|
t.Parallel() |
|
|
|
notify := newMockAliasNotify() |
|
chkID := structs.NewCheckID("foo", nil) |
|
rpc := &mockRPC{} |
|
chk := &CheckAlias{ |
|
Node: "remote", |
|
ServiceID: structs.ServiceID{ID: "web"}, |
|
CheckID: chkID, |
|
Notify: notify, |
|
RPC: rpc, |
|
} |
|
|
|
rpc.AddReply("Health.NodeChecks", structs.IndexedHealthChecks{ |
|
HealthChecks: []*structs.HealthCheck{ |
|
// Should ignore non-matching node |
|
{ |
|
Node: "A", |
|
ServiceID: "web", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Should ignore non-matching service |
|
{ |
|
Node: "remote", |
|
ServiceID: "db", |
|
Status: api.HealthCritical, |
|
}, |
|
}, |
|
}) |
|
|
|
injected := structs.IndexedNodeServices{ |
|
NodeServices: &structs.NodeServices{ |
|
Node: &structs.Node{ |
|
Node: "remote", |
|
}, |
|
Services: make(map[string]*structs.NodeService), |
|
}, |
|
QueryMeta: structs.QueryMeta{}, |
|
} |
|
rpc.AddReply("Catalog.NodeServices", injected) |
|
|
|
chk.Start() |
|
defer chk.Stop() |
|
retry.Run(t, func(r *retry.R) { |
|
if got, want := notify.State(chkID), api.HealthCritical; got != want { |
|
r.Fatalf("got state %q want %q", got, want) |
|
} |
|
}) |
|
} |
|
|
|
// If any checks are critical, it should be critical |
|
func TestCheckAlias_remoteCritical(t *testing.T) { |
|
t.Parallel() |
|
|
|
notify := newMockAliasNotify() |
|
chkID := structs.NewCheckID("foo", nil) |
|
rpc := &mockRPC{} |
|
chk := &CheckAlias{ |
|
Node: "remote", |
|
ServiceID: structs.ServiceID{ID: "web"}, |
|
CheckID: chkID, |
|
Notify: notify, |
|
RPC: rpc, |
|
} |
|
|
|
rpc.AddReply("Health.NodeChecks", structs.IndexedHealthChecks{ |
|
HealthChecks: []*structs.HealthCheck{ |
|
// Should ignore non-matching node |
|
{ |
|
Node: "A", |
|
ServiceID: "web", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Should ignore non-matching service |
|
{ |
|
Node: "remote", |
|
ServiceID: "db", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Match |
|
{ |
|
Node: "remote", |
|
ServiceID: "web", |
|
Status: api.HealthPassing, |
|
}, |
|
|
|
{ |
|
Node: "remote", |
|
ServiceID: "web", |
|
Status: api.HealthCritical, |
|
}, |
|
}, |
|
}) |
|
|
|
chk.Start() |
|
defer chk.Stop() |
|
retry.Run(t, func(r *retry.R) { |
|
if got, want := notify.State(chkID), api.HealthCritical; got != want { |
|
r.Fatalf("got state %q want %q", got, want) |
|
} |
|
}) |
|
} |
|
|
|
// If no checks are critical and at least one is warning, then it should warn |
|
func TestCheckAlias_remoteWarning(t *testing.T) { |
|
t.Parallel() |
|
|
|
notify := newMockAliasNotify() |
|
chkID := structs.NewCheckID("foo", nil) |
|
rpc := &mockRPC{} |
|
chk := &CheckAlias{ |
|
Node: "remote", |
|
ServiceID: structs.NewServiceID("web", nil), |
|
CheckID: chkID, |
|
Notify: notify, |
|
RPC: rpc, |
|
} |
|
|
|
rpc.AddReply("Health.NodeChecks", structs.IndexedHealthChecks{ |
|
HealthChecks: []*structs.HealthCheck{ |
|
// Should ignore non-matching node |
|
{ |
|
Node: "A", |
|
ServiceID: "web", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Should ignore non-matching service |
|
{ |
|
Node: "remote", |
|
ServiceID: "db", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Match |
|
{ |
|
Node: "remote", |
|
ServiceID: "web", |
|
Status: api.HealthPassing, |
|
}, |
|
|
|
{ |
|
Node: "remote", |
|
ServiceID: "web", |
|
Status: api.HealthWarning, |
|
}, |
|
}, |
|
}) |
|
|
|
chk.Start() |
|
defer chk.Stop() |
|
retry.Run(t, func(r *retry.R) { |
|
if got, want := notify.State(chkID), api.HealthWarning; got != want { |
|
r.Fatalf("got state %q want %q", got, want) |
|
} |
|
}) |
|
} |
|
|
|
// Only passing should result in passing for node-only checks |
|
func TestCheckAlias_remoteNodeOnlyPassing(t *testing.T) { |
|
t.Parallel() |
|
|
|
notify := newMockAliasNotify() |
|
chkID := structs.NewCheckID(types.CheckID("foo"), nil) |
|
rpc := &mockRPC{} |
|
chk := &CheckAlias{ |
|
Node: "remote", |
|
CheckID: chkID, |
|
Notify: notify, |
|
RPC: rpc, |
|
} |
|
|
|
rpc.AddReply("Health.NodeChecks", structs.IndexedHealthChecks{ |
|
HealthChecks: []*structs.HealthCheck{ |
|
// Should ignore non-matching node |
|
{ |
|
Node: "A", |
|
ServiceID: "web", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Should ignore any services |
|
{ |
|
Node: "remote", |
|
ServiceID: "db", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Match |
|
{ |
|
Node: "remote", |
|
Status: api.HealthPassing, |
|
}, |
|
}, |
|
}) |
|
|
|
chk.Start() |
|
defer chk.Stop() |
|
retry.Run(t, func(r *retry.R) { |
|
if got, want := notify.State(chkID), api.HealthPassing; got != want { |
|
r.Fatalf("got state %q want %q", got, want) |
|
} |
|
}) |
|
} |
|
|
|
// Only critical should result in passing for node-only checks |
|
func TestCheckAlias_remoteNodeOnlyCritical(t *testing.T) { |
|
t.Parallel() |
|
|
|
run := func(t *testing.T, responseNodeName string) { |
|
notify := newMockAliasNotify() |
|
chkID := structs.NewCheckID(types.CheckID("foo"), nil) |
|
rpc := &mockRPC{} |
|
chk := &CheckAlias{ |
|
Node: "remote", |
|
CheckID: chkID, |
|
Notify: notify, |
|
RPC: rpc, |
|
} |
|
|
|
rpc.AddReply("Health.NodeChecks", structs.IndexedHealthChecks{ |
|
HealthChecks: []*structs.HealthCheck{ |
|
// Should ignore non-matching node |
|
{ |
|
Node: "A", |
|
ServiceID: "web", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Should ignore any services |
|
{ |
|
Node: responseNodeName, |
|
ServiceID: "db", |
|
Status: api.HealthCritical, |
|
}, |
|
|
|
// Match |
|
{ |
|
Node: responseNodeName, |
|
Status: api.HealthCritical, |
|
}, |
|
}, |
|
}) |
|
|
|
chk.Start() |
|
defer chk.Stop() |
|
retry.Run(t, func(r *retry.R) { |
|
if got, want := notify.State(chkID), api.HealthCritical; got != want { |
|
r.Fatalf("got state %q want %q", got, want) |
|
} |
|
}) |
|
} |
|
|
|
t.Run("same case node name", func(t *testing.T) { |
|
run(t, "remote") |
|
}) |
|
t.Run("lowercase node name", func(t *testing.T) { |
|
run(t, "ReMoTe") |
|
}) |
|
} |
|
|
|
type mockAliasNotify struct { |
|
*mock.Notify |
|
} |
|
|
|
func newMockAliasNotify() *mockAliasNotify { |
|
return &mockAliasNotify{ |
|
Notify: mock.NewNotify(), |
|
} |
|
} |
|
|
|
func (m *mockAliasNotify) AddAliasCheck(chkID structs.CheckID, serviceID structs.ServiceID, ch chan<- struct{}) error { |
|
return nil |
|
} |
|
|
|
func (m *mockAliasNotify) RemoveAliasCheck(chkID structs.CheckID, serviceID structs.ServiceID) { |
|
} |
|
|
|
func (m *mockAliasNotify) Checks(*acl.EnterpriseMeta) map[structs.CheckID]*structs.HealthCheck { |
|
return nil |
|
} |
|
|
|
// mockRPC is an implementation of RPC that can be used for tests. The |
|
// atomic.Value fields can be set concurrently and will reflect on the next |
|
// RPC call. |
|
type mockRPC struct { |
|
Calls uint32 // Read-only, number of RPC calls |
|
Args atomic.Value // Read-only, the last args sent |
|
|
|
// Write-only, the replies to send, indexed per method. If of type "error" then an error will |
|
// be returned from the RPC call. |
|
Replies map[string]*atomic.Value |
|
} |
|
|
|
func (m *mockRPC) AddReply(method string, reply interface{}) { |
|
if m.Replies == nil { |
|
m.Replies = make(map[string]*atomic.Value) |
|
} |
|
val := &atomic.Value{} |
|
val.Store(reply) |
|
m.Replies[method] = val |
|
|
|
} |
|
|
|
func (m *mockRPC) RPC(ctx context.Context, method string, args interface{}, reply interface{}) error { |
|
atomic.AddUint32(&m.Calls, 1) |
|
m.Args.Store(args) |
|
|
|
// We don't adhere to blocking queries, so this helps prevent |
|
// too much CPU usage on the check loop. |
|
time.Sleep(10 * time.Millisecond) |
|
|
|
// This whole machinery below sets the value of the reply. This is |
|
// basically what net/rpc does internally, though much condensed. |
|
replyv := reflect.ValueOf(reply) |
|
if replyv.Kind() != reflect.Ptr { |
|
return fmt.Errorf("RPC reply must be pointer") |
|
} |
|
replyv = replyv.Elem() // Get pointer value |
|
replyv.Set(reflect.Zero(replyv.Type())) // Reset to zero value |
|
repl := m.Replies[method] |
|
if repl == nil { |
|
return fmt.Errorf("No Such Method: %s", method) |
|
} |
|
if v := m.Replies[method].Load(); v != nil { |
|
// Return an error if the reply is an error type |
|
if err, ok := v.(error); ok { |
|
return err |
|
} |
|
replyv.Set(reflect.ValueOf(v)) // Set to reply value if non-nil |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// Test that local checks immediately reflect the subject states when added and |
|
// don't require an update to the subject before being accurate. |
|
func TestCheckAlias_localInitialStatus(t *testing.T) { |
|
t.Parallel() |
|
|
|
notify := newMockAliasNotify() |
|
// We fake a local service web to ensure check if passing works |
|
notify.Notify.AddServiceID(structs.ServiceID{ID: "web"}) |
|
chkID := structs.NewCheckID(types.CheckID("foo"), nil) |
|
rpc := &mockRPC{} |
|
chk := &CheckAlias{ |
|
ServiceID: structs.ServiceID{ID: "web"}, |
|
CheckID: chkID, |
|
Notify: notify, |
|
RPC: rpc, |
|
} |
|
|
|
chk.Start() |
|
defer chk.Stop() |
|
|
|
// Don't touch the aliased service or it's checks (there are none but this is |
|
// valid and should be consisded "passing"). |
|
|
|
retry.Run(t, func(r *retry.R) { |
|
if got, want := notify.State(chkID), api.HealthPassing; got != want { |
|
r.Fatalf("got state %q want %q", got, want) |
|
} |
|
}) |
|
} |
|
|
|
// Local check on non-existing service |
|
func TestCheckAlias_localInitialStatusShouldFailBecauseNoService(t *testing.T) { |
|
t.Parallel() |
|
|
|
notify := newMockAliasNotify() |
|
chkID := structs.NewCheckID(types.CheckID("foo"), nil) |
|
rpc := &mockRPC{} |
|
chk := &CheckAlias{ |
|
ServiceID: structs.ServiceID{ID: "web"}, |
|
CheckID: chkID, |
|
Notify: notify, |
|
RPC: rpc, |
|
} |
|
|
|
chk.Start() |
|
defer chk.Stop() |
|
|
|
retry.Run(t, func(r *retry.R) { |
|
if got, want := notify.State(chkID), api.HealthCritical; got != want { |
|
r.Fatalf("got state %q want %q", got, want) |
|
} |
|
}) |
|
}
|
|
|