NET 6354 - Add tenancy in Node Health Controller (#19457)

* node health controller tenancy

* some prog

* some fixes

* revert

* pr comment resolved

* removed name

* cleanup nodes

* some fixes

* merge main
This commit is contained in:
Ashesh Vidyut 2023-11-08 13:01:17 +05:30 committed by GitHub
parent 7bc2581c81
commit 985aa76da3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 272 additions and 193 deletions

View File

@ -6,6 +6,7 @@ package nodehealth
import ( import (
"context" "context"
"fmt" "fmt"
"github.com/hashicorp/consul/agent/structs"
"testing" "testing"
"github.com/oklog/ulid/v2" "github.com/oklog/ulid/v2"
@ -14,6 +15,7 @@ import (
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
mockres "github.com/hashicorp/consul/agent/grpc-external/services/resource"
svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing" svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing"
"github.com/hashicorp/consul/internal/catalog/internal/types" "github.com/hashicorp/consul/internal/catalog/internal/types"
"github.com/hashicorp/consul/internal/controller" "github.com/hashicorp/consul/internal/controller"
@ -45,22 +47,18 @@ var (
} }
) )
func resourceID(rtype *pbresource.Type, name string) *pbresource.ID { func resourceID(rtype *pbresource.Type, name string, tenancy *pbresource.Tenancy) *pbresource.ID {
return &pbresource.ID{ return &pbresource.ID{
Type: rtype, Type: rtype,
Tenancy: &pbresource.Tenancy{ Tenancy: tenancy,
Partition: "default", Name: name,
Namespace: "default",
PeerName: "local",
},
Name: name,
} }
} }
type nodeHealthControllerTestSuite struct { type nodeHealthControllerTestSuite struct {
suite.Suite suite.Suite
resourceClient pbresource.ResourceServiceClient resourceClient *resourcetest.Client
runtime controller.Runtime runtime controller.Runtime
ctl nodeHealthReconciler ctl nodeHealthReconciler
@ -70,159 +68,144 @@ type nodeHealthControllerTestSuite struct {
nodeWarning *pbresource.ID nodeWarning *pbresource.ID
nodeCritical *pbresource.ID nodeCritical *pbresource.ID
nodeMaintenance *pbresource.ID nodeMaintenance *pbresource.ID
isEnterprise bool
tenancies []*pbresource.Tenancy
}
func (suite *nodeHealthControllerTestSuite) writeNode(name string, tenancy *pbresource.Tenancy) *pbresource.ID {
return resourcetest.Resource(pbcatalog.NodeType, name).
WithData(suite.T(), nodeData).
WithTenancy(tenancy).
Write(suite.T(), suite.resourceClient).Id
} }
func (suite *nodeHealthControllerTestSuite) SetupTest() { func (suite *nodeHealthControllerTestSuite) SetupTest() {
suite.resourceClient = svctest.RunResourceService(suite.T(), types.Register, types.RegisterDNSPolicy) mockTenancyBridge := &mockres.MockTenancyBridge{}
suite.tenancies = resourcetest.TestTenancies()
for _, tenancy := range suite.tenancies {
mockTenancyBridge.On("PartitionExists", tenancy.Partition).Return(true, nil)
mockTenancyBridge.On("NamespaceExists", tenancy.Partition, tenancy.Namespace).Return(true, nil)
mockTenancyBridge.On("IsPartitionMarkedForDeletion", tenancy.Partition).Return(false, nil)
mockTenancyBridge.On("IsNamespaceMarkedForDeletion", tenancy.Partition, tenancy.Namespace).Return(false, nil)
}
cfg := mockres.Config{
TenancyBridge: mockTenancyBridge,
}
client := svctest.RunResourceServiceWithConfig(suite.T(), cfg, types.Register, types.RegisterDNSPolicy)
suite.resourceClient = resourcetest.NewClient(client)
suite.runtime = controller.Runtime{Client: suite.resourceClient, Logger: testutil.Logger(suite.T())} suite.runtime = controller.Runtime{Client: suite.resourceClient, Logger: testutil.Logger(suite.T())}
suite.isEnterprise = structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty() == "default"
// The rest of the setup will be to prime the resource service with some data
suite.nodeNoHealth = resourcetest.Resource(pbcatalog.NodeType, "test-node-no-health").
WithData(suite.T(), nodeData).
Write(suite.T(), suite.resourceClient).Id
suite.nodePassing = resourcetest.Resource(pbcatalog.NodeType, "test-node-passing").
WithData(suite.T(), nodeData).
Write(suite.T(), suite.resourceClient).Id
suite.nodeWarning = resourcetest.Resource(pbcatalog.NodeType, "test-node-warning").
WithData(suite.T(), nodeData).
Write(suite.T(), suite.resourceClient).Id
suite.nodeCritical = resourcetest.Resource(pbcatalog.NodeType, "test-node-critical").
WithData(suite.T(), nodeData).
Write(suite.T(), suite.resourceClient).Id
suite.nodeMaintenance = resourcetest.Resource(pbcatalog.NodeType, "test-node-maintenance").
WithData(suite.T(), nodeData).
Write(suite.T(), suite.resourceClient).Id
nodeHealthDesiredStatus := map[string]pbcatalog.Health{
suite.nodePassing.Name: pbcatalog.Health_HEALTH_PASSING,
suite.nodeWarning.Name: pbcatalog.Health_HEALTH_WARNING,
suite.nodeCritical.Name: pbcatalog.Health_HEALTH_CRITICAL,
suite.nodeMaintenance.Name: pbcatalog.Health_HEALTH_MAINTENANCE,
}
// In order to exercise the behavior to ensure that its not a last-status-wins sort of thing
// we are strategically naming health statuses so that they will be returned in an order with
// the most precedent status being in the middle of the list. This will ensure that statuses
// seen later can overide a previous status and that statuses seen later do not override if
// they would lower the overall status such as going from critical -> warning.
precedenceHealth := []pbcatalog.Health{
pbcatalog.Health_HEALTH_PASSING,
pbcatalog.Health_HEALTH_WARNING,
pbcatalog.Health_HEALTH_CRITICAL,
pbcatalog.Health_HEALTH_MAINTENANCE,
pbcatalog.Health_HEALTH_CRITICAL,
pbcatalog.Health_HEALTH_WARNING,
pbcatalog.Health_HEALTH_PASSING,
}
for _, node := range []*pbresource.ID{suite.nodePassing, suite.nodeWarning, suite.nodeCritical, suite.nodeMaintenance} {
for idx, health := range precedenceHealth {
if nodeHealthDesiredStatus[node.Name] >= health {
resourcetest.Resource(pbcatalog.HealthStatusType, fmt.Sprintf("test-check-%s-%d", node.Name, idx)).
WithData(suite.T(), &pbcatalog.HealthStatus{Type: "tcp", Status: health}).
WithOwner(node).
Write(suite.T(), suite.resourceClient)
}
}
}
// create a DNSPolicy to be owned by the node. The type doesn't really matter it just needs
// to be something that doesn't care about its owner. All we want to prove is that we are
// filtering out non-HealthStatus types appropriately.
resourcetest.Resource(pbcatalog.DNSPolicyType, "test-policy").
WithData(suite.T(), dnsPolicyData).
WithOwner(suite.nodeNoHealth).
Write(suite.T(), suite.resourceClient)
} }
func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthListError() { func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthListError() {
// This resource id references a resource type that will not be suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
// registered with the resource service. The ListByOwner call // This resource id references a resource type that will not be
// should produce an InvalidArgument error. This test is meant // registered with the resource service. The ListByOwner call
// to validate how that error is handled (its propagated back // should produce an InvalidArgument error. This test is meant
// to the caller) // to validate how that error is handled (its propagated back
ref := resourceID( // to the caller)
&pbresource.Type{Group: "not", GroupVersion: "v1", Kind: "found"}, ref := resourceID(
"irrelevant", &pbresource.Type{Group: "not", GroupVersion: "v1", Kind: "found"},
) "irrelevant",
health, err := getNodeHealth(context.Background(), suite.runtime, ref) tenancy,
require.Equal(suite.T(), pbcatalog.Health_HEALTH_CRITICAL, health) )
require.Error(suite.T(), err) health, err := getNodeHealth(context.Background(), suite.runtime, ref)
require.Equal(suite.T(), codes.InvalidArgument, status.Code(err)) require.Equal(suite.T(), pbcatalog.Health_HEALTH_CRITICAL, health)
require.Error(suite.T(), err)
require.Equal(suite.T(), codes.InvalidArgument, status.Code(err))
})
} }
func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthNoNode() { func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthNoNode() {
// This test is meant to ensure that when the node doesn't exist suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
// no error is returned but also no data is. The default passing // This test is meant to ensure that when the node doesn't exist
// status should then be returned in the same manner as the node // no error is returned but also no data is. The default passing
// existing but with no associated HealthStatus resources. // status should then be returned in the same manner as the node
ref := resourceID(pbcatalog.NodeType, "foo") // existing but with no associated HealthStatus resources.
ref.Uid = ulid.Make().String() ref := resourceID(pbcatalog.NodeType, "foo", tenancy)
health, err := getNodeHealth(context.Background(), suite.runtime, ref) ref.Uid = ulid.Make().String()
health, err := getNodeHealth(context.Background(), suite.runtime, ref)
require.NoError(suite.T(), err) require.NoError(suite.T(), err)
require.Equal(suite.T(), pbcatalog.Health_HEALTH_PASSING, health) require.Equal(suite.T(), pbcatalog.Health_HEALTH_PASSING, health)
})
} }
func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthNoStatus() { func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthNoStatus() {
health, err := getNodeHealth(context.Background(), suite.runtime, suite.nodeNoHealth) suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
require.NoError(suite.T(), err)
require.Equal(suite.T(), pbcatalog.Health_HEALTH_PASSING, health) health, err := getNodeHealth(context.Background(), suite.runtime, suite.nodeNoHealth)
require.NoError(suite.T(), err)
require.Equal(suite.T(), pbcatalog.Health_HEALTH_PASSING, health)
})
} }
func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthPassingStatus() { func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthPassingStatus() {
health, err := getNodeHealth(context.Background(), suite.runtime, suite.nodePassing) suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
require.NoError(suite.T(), err)
require.Equal(suite.T(), pbcatalog.Health_HEALTH_PASSING, health) health, err := getNodeHealth(context.Background(), suite.runtime, suite.nodePassing)
require.NoError(suite.T(), err)
require.Equal(suite.T(), pbcatalog.Health_HEALTH_PASSING, health)
})
} }
func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthCriticalStatus() { func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthCriticalStatus() {
health, err := getNodeHealth(context.Background(), suite.runtime, suite.nodeCritical) suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
require.NoError(suite.T(), err)
require.Equal(suite.T(), pbcatalog.Health_HEALTH_CRITICAL, health) health, err := getNodeHealth(context.Background(), suite.runtime, suite.nodeCritical)
require.NoError(suite.T(), err)
require.Equal(suite.T(), pbcatalog.Health_HEALTH_CRITICAL, health)
})
} }
func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthWarningStatus() { func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthWarningStatus() {
health, err := getNodeHealth(context.Background(), suite.runtime, suite.nodeWarning) suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
require.NoError(suite.T(), err)
require.Equal(suite.T(), pbcatalog.Health_HEALTH_WARNING, health) health, err := getNodeHealth(context.Background(), suite.runtime, suite.nodeWarning)
require.NoError(suite.T(), err)
require.Equal(suite.T(), pbcatalog.Health_HEALTH_WARNING, health)
})
} }
func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthMaintenanceStatus() { func (suite *nodeHealthControllerTestSuite) TestGetNodeHealthMaintenanceStatus() {
health, err := getNodeHealth(context.Background(), suite.runtime, suite.nodeMaintenance) suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
require.NoError(suite.T(), err)
require.Equal(suite.T(), pbcatalog.Health_HEALTH_MAINTENANCE, health) health, err := getNodeHealth(context.Background(), suite.runtime, suite.nodeMaintenance)
require.NoError(suite.T(), err)
require.Equal(suite.T(), pbcatalog.Health_HEALTH_MAINTENANCE, health)
})
} }
func (suite *nodeHealthControllerTestSuite) TestReconcileNodeNotFound() { func (suite *nodeHealthControllerTestSuite) TestReconcileNodeNotFound() {
// This test ensures that removed nodes are ignored. In particular we don't suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
// want to propagate the error and indefinitely keep re-reconciling in this case. // This test ensures that removed nodes are ignored. In particular we don't
err := suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ // want to propagate the error and indefinitely keep re-reconciling in this case.
ID: resourceID(pbcatalog.NodeType, "not-found"), err := suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{
ID: resourceID(pbcatalog.NodeType, "not-found", tenancy),
})
require.NoError(suite.T(), err)
}) })
require.NoError(suite.T(), err)
} }
func (suite *nodeHealthControllerTestSuite) TestReconcilePropagateReadError() { func (suite *nodeHealthControllerTestSuite) TestReconcilePropagateReadError() {
// This test aims to ensure that errors other than NotFound errors coming suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
// from the initial resource read get propagated. This case is very unrealistic // This test aims to ensure that errors other than NotFound errors coming
// as the controller should not have given us a request ID for a resource type // from the initial resource read get propagated. This case is very unrealistic
// that doesn't exist but this was the easiest way I could think of to synthesize // as the controller should not have given us a request ID for a resource type
// a Read error. // that doesn't exist but this was the easiest way I could think of to synthesize
ref := resourceID( // a Read error.
&pbresource.Type{Group: "not", GroupVersion: "v1", Kind: "found"}, ref := resourceID(
"irrelevant", &pbresource.Type{Group: "not", GroupVersion: "v1", Kind: "found"},
) "irrelevant",
tenancy,
)
err := suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{ err := suite.ctl.Reconcile(context.Background(), suite.runtime, controller.Request{
ID: ref, ID: ref,
})
require.Error(suite.T(), err)
require.Equal(suite.T(), codes.InvalidArgument, status.Code(err))
}) })
require.Error(suite.T(), err)
require.Equal(suite.T(), codes.InvalidArgument, status.Code(err))
} }
func (suite *nodeHealthControllerTestSuite) testReconcileStatus(id *pbresource.ID, expectedStatus *pbresource.Condition) *pbresource.Resource { func (suite *nodeHealthControllerTestSuite) testReconcileStatus(id *pbresource.ID, expectedStatus *pbresource.Condition) *pbresource.Resource {
@ -250,60 +233,75 @@ func (suite *nodeHealthControllerTestSuite) testReconcileStatus(id *pbresource.I
} }
func (suite *nodeHealthControllerTestSuite) TestReconcile_StatusPassing() { func (suite *nodeHealthControllerTestSuite) TestReconcile_StatusPassing() {
suite.testReconcileStatus(suite.nodePassing, &pbresource.Condition{ suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
Type: StatusConditionHealthy,
State: pbresource.Condition_STATE_TRUE, suite.testReconcileStatus(suite.nodePassing, &pbresource.Condition{
Reason: "HEALTH_PASSING", Type: StatusConditionHealthy,
Message: NodeHealthyMessage, State: pbresource.Condition_STATE_TRUE,
Reason: "HEALTH_PASSING",
Message: NodeHealthyMessage,
})
}) })
} }
func (suite *nodeHealthControllerTestSuite) TestReconcile_StatusWarning() { func (suite *nodeHealthControllerTestSuite) TestReconcile_StatusWarning() {
suite.testReconcileStatus(suite.nodeWarning, &pbresource.Condition{ suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
Type: StatusConditionHealthy,
State: pbresource.Condition_STATE_FALSE, suite.testReconcileStatus(suite.nodeWarning, &pbresource.Condition{
Reason: "HEALTH_WARNING", Type: StatusConditionHealthy,
Message: NodeUnhealthyMessage, State: pbresource.Condition_STATE_FALSE,
Reason: "HEALTH_WARNING",
Message: NodeUnhealthyMessage,
})
}) })
} }
func (suite *nodeHealthControllerTestSuite) TestReconcile_StatusCritical() { func (suite *nodeHealthControllerTestSuite) TestReconcile_StatusCritical() {
suite.testReconcileStatus(suite.nodeCritical, &pbresource.Condition{ suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
Type: StatusConditionHealthy,
State: pbresource.Condition_STATE_FALSE, suite.testReconcileStatus(suite.nodeCritical, &pbresource.Condition{
Reason: "HEALTH_CRITICAL", Type: StatusConditionHealthy,
Message: NodeUnhealthyMessage, State: pbresource.Condition_STATE_FALSE,
Reason: "HEALTH_CRITICAL",
Message: NodeUnhealthyMessage,
})
}) })
} }
func (suite *nodeHealthControllerTestSuite) TestReconcile_StatusMaintenance() { func (suite *nodeHealthControllerTestSuite) TestReconcile_StatusMaintenance() {
suite.testReconcileStatus(suite.nodeMaintenance, &pbresource.Condition{ suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
Type: StatusConditionHealthy,
State: pbresource.Condition_STATE_FALSE, suite.testReconcileStatus(suite.nodeMaintenance, &pbresource.Condition{
Reason: "HEALTH_MAINTENANCE", Type: StatusConditionHealthy,
Message: NodeUnhealthyMessage, State: pbresource.Condition_STATE_FALSE,
Reason: "HEALTH_MAINTENANCE",
Message: NodeUnhealthyMessage,
})
}) })
} }
func (suite *nodeHealthControllerTestSuite) TestReconcile_AvoidRereconciliationWrite() { func (suite *nodeHealthControllerTestSuite) TestReconcile_AvoidRereconciliationWrite() {
res1 := suite.testReconcileStatus(suite.nodeWarning, &pbresource.Condition{ suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
Type: StatusConditionHealthy,
State: pbresource.Condition_STATE_FALSE,
Reason: "HEALTH_WARNING",
Message: NodeUnhealthyMessage,
})
res2 := suite.testReconcileStatus(suite.nodeWarning, &pbresource.Condition{ res1 := suite.testReconcileStatus(suite.nodeWarning, &pbresource.Condition{
Type: StatusConditionHealthy, Type: StatusConditionHealthy,
State: pbresource.Condition_STATE_FALSE, State: pbresource.Condition_STATE_FALSE,
Reason: "HEALTH_WARNING", Reason: "HEALTH_WARNING",
Message: NodeUnhealthyMessage, Message: NodeUnhealthyMessage,
}) })
// If another status write was performed then the versions would differ. This res2 := suite.testReconcileStatus(suite.nodeWarning, &pbresource.Condition{
// therefore proves that after a second reconciliation without any change in status Type: StatusConditionHealthy,
// that we are not making subsequent status writes. State: pbresource.Condition_STATE_FALSE,
require.Equal(suite.T(), res1.Version, res2.Version) Reason: "HEALTH_WARNING",
Message: NodeUnhealthyMessage,
})
// If another status write was performed then the versions would differ. This
// therefore proves that after a second reconciliation without any change in status
// that we are not making subsequent status writes.
require.Equal(suite.T(), res1.Version, res2.Version)
})
} }
func (suite *nodeHealthControllerTestSuite) waitForReconciliation(id *pbresource.ID, reason string) { func (suite *nodeHealthControllerTestSuite) waitForReconciliation(id *pbresource.ID, reason string) {
@ -323,44 +321,125 @@ func (suite *nodeHealthControllerTestSuite) waitForReconciliation(id *pbresource
}) })
} }
func (suite *nodeHealthControllerTestSuite) TestController() { func (suite *nodeHealthControllerTestSuite) TestController() {
// create the controller manager suite.runTestCaseWithTenancies(func(tenancy *pbresource.Tenancy) {
mgr := controller.NewManager(suite.resourceClient, testutil.Logger(suite.T()))
// register our controller // create the controller manager
mgr.Register(NodeHealthController()) mgr := controller.NewManager(suite.resourceClient, testutil.Logger(suite.T()))
mgr.SetRaftLeader(true)
ctx, cancel := context.WithCancel(context.Background())
suite.T().Cleanup(cancel)
// run the manager // register our controller
go mgr.Run(ctx) mgr.Register(NodeHealthController())
mgr.SetRaftLeader(true)
ctx, cancel := context.WithCancel(context.Background())
suite.T().Cleanup(cancel)
// ensure that the node health eventually gets set. // run the manager
suite.waitForReconciliation(suite.nodePassing, "HEALTH_PASSING") go mgr.Run(ctx)
// rewrite the resource - this will cause the nodes health // ensure that the node health eventually gets set.
// to be rereconciled but wont result in any health change suite.waitForReconciliation(suite.nodePassing, "HEALTH_PASSING")
resourcetest.Resource(pbcatalog.NodeType, suite.nodePassing.Name).
WithData(suite.T(), &pbcatalog.Node{ // rewrite the resource - this will cause the nodes health
Addresses: []*pbcatalog.NodeAddress{ // to be rereconciled but wont result in any health change
{ resourcetest.Resource(pbcatalog.NodeType, suite.nodePassing.Name).
Host: "198.18.0.1", WithData(suite.T(), &pbcatalog.Node{
Addresses: []*pbcatalog.NodeAddress{
{
Host: "198.18.0.1",
},
}, },
}, }).
}). WithTenancy(tenancy).
Write(suite.T(), suite.resourceClient) Write(suite.T(), suite.resourceClient)
// wait for rereconciliation to happen // wait for rereconciliation to happen
suite.waitForReconciliation(suite.nodePassing, "HEALTH_PASSING") suite.waitForReconciliation(suite.nodePassing, "HEALTH_PASSING")
resourcetest.Resource(pbcatalog.HealthStatusType, "failure"). resourcetest.Resource(pbcatalog.HealthStatusType, "failure").
WithData(suite.T(), &pbcatalog.HealthStatus{Type: "fake", Status: pbcatalog.Health_HEALTH_CRITICAL}). WithData(suite.T(), &pbcatalog.HealthStatus{Type: "fake", Status: pbcatalog.Health_HEALTH_CRITICAL}).
WithOwner(suite.nodePassing). WithOwner(suite.nodePassing).
Write(suite.T(), suite.resourceClient) WithTenancy(tenancy).
Write(suite.T(), suite.resourceClient)
suite.waitForReconciliation(suite.nodePassing, "HEALTH_CRITICAL") suite.waitForReconciliation(suite.nodePassing, "HEALTH_CRITICAL")
})
} }
func TestNodeHealthController(t *testing.T) { func TestNodeHealthController(t *testing.T) {
suite.Run(t, new(nodeHealthControllerTestSuite)) suite.Run(t, new(nodeHealthControllerTestSuite))
} }
func (suite *nodeHealthControllerTestSuite) appendTenancyInfo(tenancy *pbresource.Tenancy) string {
return fmt.Sprintf("%s_Namespace_%s_Partition", tenancy.Namespace, tenancy.Partition)
}
func (suite *nodeHealthControllerTestSuite) setupNodesWithTenancy(tenancy *pbresource.Tenancy) {
// The rest of the setup will be to prime the resource service with some data
suite.nodeNoHealth = suite.writeNode("test-node-no-health", tenancy)
suite.nodePassing = suite.writeNode("test-node-passing", tenancy)
suite.nodeWarning = suite.writeNode("test-node-warning", tenancy)
suite.nodeCritical = suite.writeNode("test-node-critical", tenancy)
suite.nodeMaintenance = suite.writeNode("test-node-maintenance", tenancy)
nodeHealthDesiredStatus := map[string]pbcatalog.Health{
suite.nodePassing.Name: pbcatalog.Health_HEALTH_PASSING,
suite.nodeWarning.Name: pbcatalog.Health_HEALTH_WARNING,
suite.nodeCritical.Name: pbcatalog.Health_HEALTH_CRITICAL,
suite.nodeMaintenance.Name: pbcatalog.Health_HEALTH_MAINTENANCE,
}
// In order to exercise the behavior to ensure that its not a last-status-wins sort of thing
// we are strategically naming health statuses so that they will be returned in an order with
// the most precedent status being in the middle of the list. This will ensure that statuses
// seen later can overide a previous status and that statuses seen later do not override if
// they would lower the overall status such as going from critical -> warning.
precedenceHealth := []pbcatalog.Health{
pbcatalog.Health_HEALTH_PASSING,
pbcatalog.Health_HEALTH_WARNING,
pbcatalog.Health_HEALTH_CRITICAL,
pbcatalog.Health_HEALTH_MAINTENANCE,
pbcatalog.Health_HEALTH_CRITICAL,
pbcatalog.Health_HEALTH_WARNING,
pbcatalog.Health_HEALTH_PASSING,
}
for _, node := range []*pbresource.ID{suite.nodePassing, suite.nodeWarning, suite.nodeCritical, suite.nodeMaintenance} {
for idx, health := range precedenceHealth {
if nodeHealthDesiredStatus[node.Name] >= health {
resourcetest.Resource(pbcatalog.HealthStatusType, fmt.Sprintf("test-check-%s-%d-%s-%s", node.Name, idx, tenancy.Partition, tenancy.Namespace)).
WithData(suite.T(), &pbcatalog.HealthStatus{Type: "tcp", Status: health}).
WithOwner(node).
Write(suite.T(), suite.resourceClient)
}
}
}
// create a DNSPolicy to be owned by the node. The type doesn't really matter it just needs
// to be something that doesn't care about its owner. All we want to prove is that we are
// filtering out non-HealthStatus types appropriately.
resourcetest.Resource(pbcatalog.DNSPolicyType, "test-policy-"+tenancy.Partition+"-"+tenancy.Namespace).
WithData(suite.T(), dnsPolicyData).
WithOwner(suite.nodeNoHealth).
WithTenancy(tenancy).
Write(suite.T(), suite.resourceClient)
}
func (suite *nodeHealthControllerTestSuite) cleanUpNodes() {
suite.resourceClient.MustDelete(suite.T(), suite.nodeNoHealth)
suite.resourceClient.MustDelete(suite.T(), suite.nodeCritical)
suite.resourceClient.MustDelete(suite.T(), suite.nodeWarning)
suite.resourceClient.MustDelete(suite.T(), suite.nodePassing)
suite.resourceClient.MustDelete(suite.T(), suite.nodeMaintenance)
}
func (suite *nodeHealthControllerTestSuite) runTestCaseWithTenancies(t func(*pbresource.Tenancy)) {
for _, tenancy := range suite.tenancies {
suite.Run(suite.appendTenancyInfo(tenancy), func() {
suite.setupNodesWithTenancy(tenancy)
suite.T().Cleanup(func() {
suite.cleanUpNodes()
})
t(tenancy)
})
}
}

View File

@ -4,10 +4,10 @@
package resourcetest package resourcetest
import ( import (
"github.com/hashicorp/consul/agent/structs"
"strings" "strings"
"testing" "testing"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
) )
@ -15,7 +15,7 @@ import (
// TestTenancies returns a list of tenancies which represent // TestTenancies returns a list of tenancies which represent
// the namespace and partition combinations that can be used in unit tests // the namespace and partition combinations that can be used in unit tests
func TestTenancies() []*pbresource.Tenancy { func TestTenancies() []*pbresource.Tenancy {
isEnterprise := (structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty() == "default") isEnterprise := structs.NodeEnterpriseMetaInDefaultPartition().PartitionOrEmpty() == "default"
tenancies := []*pbresource.Tenancy{Tenancy("default.default")} tenancies := []*pbresource.Tenancy{Tenancy("default.default")}
if isEnterprise { if isEnterprise {