2023-03-28 18:39:22 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
2023-08-11 13:12:13 +00:00
|
|
|
// SPDX-License-Identifier: BUSL-1.1
|
2023-03-28 18:39:22 +00:00
|
|
|
|
2021-02-25 21:22:30 +00:00
|
|
|
package health
|
2020-09-18 22:25:56 +00:00
|
|
|
|
|
|
|
import (
|
2022-03-15 14:34:46 +00:00
|
|
|
"errors"
|
2020-09-18 22:25:56 +00:00
|
|
|
"fmt"
|
2020-10-19 22:24:02 +00:00
|
|
|
"reflect"
|
2020-11-20 15:23:35 +00:00
|
|
|
"sort"
|
2021-02-08 23:54:37 +00:00
|
|
|
"strings"
|
2020-09-18 22:25:56 +00:00
|
|
|
|
2020-09-18 22:33:02 +00:00
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
2023-04-14 16:24:46 +00:00
|
|
|
"github.com/hashicorp/consul/agent/submatview"
|
2023-02-17 21:14:46 +00:00
|
|
|
"github.com/hashicorp/consul/proto/private/pbservice"
|
|
|
|
"github.com/hashicorp/consul/proto/private/pbsubscribe"
|
2023-04-14 16:24:46 +00:00
|
|
|
"github.com/hashicorp/go-bexpr"
|
2020-09-18 22:25:56 +00:00
|
|
|
)
|
|
|
|
|
2022-07-12 10:37:48 +00:00
|
|
|
func NewMaterializerRequest(srvReq structs.ServiceSpecificRequest) func(index uint64) *pbsubscribe.SubscribeRequest {
|
2022-03-30 16:51:56 +00:00
|
|
|
return func(index uint64) *pbsubscribe.SubscribeRequest {
|
|
|
|
req := &pbsubscribe.SubscribeRequest{
|
proxycfg: server-local config entry data sources
This is the OSS portion of enterprise PR 2056.
This commit provides server-local implementations of the proxycfg.ConfigEntry
and proxycfg.ConfigEntryList interfaces, that source data from streaming events.
It makes use of the LocalMaterializer type introduced for peering replication,
adding the necessary support for authorization.
It also adds support for "wildcard" subscriptions (within a topic) to the event
publisher, as this is needed to fetch service-resolvers for all services when
configuring mesh gateways.
Currently, events will be emitted for just the ingress-gateway, service-resolver,
and mesh config entry types, as these are the only entries required by proxycfg
— the events will be emitted on topics named IngressGateway, ServiceResolver,
and MeshConfig topics respectively.
Though these events will only be consumed "locally" for now, they can also be
consumed via the gRPC endpoint (confirmed using grpcurl) so using them from
client agents should be a case of swapping the LocalMaterializer for an
RPCMaterializer.
2022-07-01 15:09:47 +00:00
|
|
|
Topic: pbsubscribe.Topic_ServiceHealth,
|
|
|
|
Subject: &pbsubscribe.SubscribeRequest_NamedSubject{
|
|
|
|
NamedSubject: &pbsubscribe.NamedSubject{
|
|
|
|
Key: srvReq.ServiceName,
|
|
|
|
Namespace: srvReq.EnterpriseMeta.NamespaceOrEmpty(),
|
|
|
|
Partition: srvReq.EnterpriseMeta.PartitionOrEmpty(),
|
|
|
|
PeerName: srvReq.PeerName,
|
|
|
|
},
|
|
|
|
},
|
2020-10-01 06:36:36 +00:00
|
|
|
Token: srvReq.Token,
|
|
|
|
Datacenter: srvReq.Datacenter,
|
|
|
|
Index: index,
|
|
|
|
}
|
|
|
|
if srvReq.Connect {
|
|
|
|
req.Topic = pbsubscribe.Topic_ServiceHealthConnect
|
|
|
|
}
|
|
|
|
return req
|
2020-09-18 22:25:56 +00:00
|
|
|
}
|
2020-10-03 00:04:45 +00:00
|
|
|
}
|
|
|
|
|
2022-07-12 10:37:48 +00:00
|
|
|
func NewHealthView(req structs.ServiceSpecificRequest) (*HealthView, error) {
|
2021-02-08 23:54:37 +00:00
|
|
|
fe, err := newFilterEvaluator(req)
|
2020-10-19 22:24:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-07-12 10:37:48 +00:00
|
|
|
return &HealthView{
|
2023-03-03 19:17:11 +00:00
|
|
|
state: make(map[string]structs.CheckServiceNode),
|
|
|
|
filter: fe,
|
|
|
|
connect: req.Connect,
|
|
|
|
kind: req.ServiceKind,
|
2020-10-19 22:24:02 +00:00
|
|
|
}, nil
|
2020-09-18 22:25:56 +00:00
|
|
|
}
|
|
|
|
|
2023-04-14 16:24:46 +00:00
|
|
|
var _ submatview.View = (*HealthView)(nil)
|
|
|
|
|
2022-07-12 10:37:48 +00:00
|
|
|
// HealthView implements submatview.View for storing the view state
|
2020-09-18 22:25:56 +00:00
|
|
|
// of a service health result. We store it as a map to make updates and
|
|
|
|
// deletions a little easier but we could just store a result type
|
|
|
|
// (IndexedCheckServiceNodes) and update it in place for each event - that
|
|
|
|
// involves re-sorting each time etc. though.
|
2022-07-12 10:37:48 +00:00
|
|
|
type HealthView struct {
|
2023-03-03 19:17:11 +00:00
|
|
|
connect bool
|
|
|
|
kind structs.ServiceKind
|
|
|
|
state map[string]structs.CheckServiceNode
|
|
|
|
filter filterEvaluator
|
2020-09-18 22:25:56 +00:00
|
|
|
}
|
|
|
|
|
2020-09-29 21:42:48 +00:00
|
|
|
// Update implements View
|
2022-07-12 10:37:48 +00:00
|
|
|
func (s *HealthView) Update(events []*pbsubscribe.Event) error {
|
2020-09-18 22:25:56 +00:00
|
|
|
for _, event := range events {
|
|
|
|
serviceHealth := event.GetServiceHealth()
|
|
|
|
if serviceHealth == nil {
|
|
|
|
return fmt.Errorf("unexpected event type for service health view: %T",
|
|
|
|
event.GetPayload())
|
|
|
|
}
|
|
|
|
|
2020-10-06 20:54:56 +00:00
|
|
|
id := serviceHealth.CheckServiceNode.UniqueID()
|
2020-09-18 22:25:56 +00:00
|
|
|
switch serviceHealth.Op {
|
2020-09-18 22:33:02 +00:00
|
|
|
case pbsubscribe.CatalogOp_Register:
|
2022-03-15 14:34:46 +00:00
|
|
|
csn, err := pbservice.CheckServiceNodeToStructs(serviceHealth.CheckServiceNode)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if csn == nil {
|
|
|
|
return errors.New("check service node was unexpectedly nil")
|
|
|
|
}
|
2023-03-03 19:17:11 +00:00
|
|
|
|
|
|
|
// check if we intentionally need to skip the filter
|
|
|
|
if s.skipFilter(csn) {
|
|
|
|
s.state[id] = *csn
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-03-15 14:34:46 +00:00
|
|
|
passed, err := s.filter.Evaluate(*csn)
|
2022-04-27 15:39:45 +00:00
|
|
|
if err != nil {
|
2020-10-19 22:24:02 +00:00
|
|
|
return err
|
2022-04-27 15:39:45 +00:00
|
|
|
} else if passed {
|
2022-03-15 14:34:46 +00:00
|
|
|
s.state[id] = *csn
|
2022-04-27 15:39:45 +00:00
|
|
|
} else {
|
|
|
|
delete(s.state, id)
|
2020-10-19 22:24:02 +00:00
|
|
|
}
|
|
|
|
|
2020-09-18 22:33:02 +00:00
|
|
|
case pbsubscribe.CatalogOp_Deregister:
|
2020-09-18 22:25:56 +00:00
|
|
|
delete(s.state, id)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-03-03 19:17:11 +00:00
|
|
|
func (s *HealthView) skipFilter(csn *structs.CheckServiceNode) bool {
|
|
|
|
// we only do this for connect-enabled services that need to be routed through a terminating gateway
|
|
|
|
return s.kind == "" && s.connect && csn.Service.Kind == structs.ServiceKindTerminatingGateway
|
|
|
|
}
|
|
|
|
|
2020-10-19 22:24:02 +00:00
|
|
|
type filterEvaluator interface {
|
|
|
|
Evaluate(datum interface{}) (bool, error)
|
|
|
|
}
|
|
|
|
|
2021-02-25 21:22:30 +00:00
|
|
|
func newFilterEvaluator(req structs.ServiceSpecificRequest) (filterEvaluator, error) {
|
2021-02-08 23:54:37 +00:00
|
|
|
var evaluators []filterEvaluator
|
|
|
|
|
|
|
|
typ := reflect.TypeOf(structs.CheckServiceNode{})
|
|
|
|
if req.Filter != "" {
|
|
|
|
e, err := bexpr.CreateEvaluatorForType(req.Filter, nil, typ)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
evaluators = append(evaluators, e)
|
|
|
|
}
|
|
|
|
|
|
|
|
if req.ServiceTag != "" {
|
|
|
|
// Handle backwards compat with old field
|
|
|
|
req.ServiceTags = []string{req.ServiceTag}
|
|
|
|
}
|
|
|
|
|
|
|
|
if req.TagFilter && len(req.ServiceTags) > 0 {
|
|
|
|
evaluators = append(evaluators, serviceTagEvaluator{tags: req.ServiceTags})
|
|
|
|
}
|
|
|
|
|
|
|
|
for key, value := range req.NodeMetaFilters {
|
|
|
|
expr := fmt.Sprintf(`"%s" in Node.Meta.%s`, value, key)
|
|
|
|
e, err := bexpr.CreateEvaluatorForType(expr, nil, typ)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
evaluators = append(evaluators, e)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch len(evaluators) {
|
|
|
|
case 0:
|
2020-10-19 22:24:02 +00:00
|
|
|
return noopFilterEvaluator{}, nil
|
2021-02-08 23:54:37 +00:00
|
|
|
case 1:
|
|
|
|
return evaluators[0], nil
|
|
|
|
default:
|
|
|
|
return &multiFilterEvaluator{evaluators: evaluators}, nil
|
2020-10-19 22:24:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// noopFilterEvaluator may be used in place of a bexpr.Evaluator. The Evaluate
|
|
|
|
// method always return true, so no items will be filtered out.
|
|
|
|
type noopFilterEvaluator struct{}
|
|
|
|
|
|
|
|
func (noopFilterEvaluator) Evaluate(_ interface{}) (bool, error) {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2021-02-08 23:54:37 +00:00
|
|
|
type multiFilterEvaluator struct {
|
|
|
|
evaluators []filterEvaluator
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m multiFilterEvaluator) Evaluate(data interface{}) (bool, error) {
|
|
|
|
for _, e := range m.evaluators {
|
|
|
|
match, err := e.Evaluate(data)
|
|
|
|
if !match || err != nil {
|
|
|
|
return match, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2020-11-25 20:01:07 +00:00
|
|
|
// sortCheckServiceNodes sorts the results to match memdb semantics
|
2020-11-20 15:23:35 +00:00
|
|
|
// Sort results by Node.Node, if 2 instances match, order by Service.ID
|
|
|
|
// Will allow result to be stable sorted and match queries without cache
|
2020-11-25 20:01:07 +00:00
|
|
|
func sortCheckServiceNodes(serviceNodes *structs.IndexedCheckServiceNodes) {
|
2020-11-20 15:23:35 +00:00
|
|
|
sort.SliceStable(serviceNodes.Nodes, func(i, j int) bool {
|
|
|
|
left := serviceNodes.Nodes[i]
|
|
|
|
right := serviceNodes.Nodes[j]
|
2020-11-25 20:01:07 +00:00
|
|
|
if left.Node.Node == right.Node.Node {
|
|
|
|
return left.Service.ID < right.Service.ID
|
2020-11-20 15:23:35 +00:00
|
|
|
}
|
2020-11-25 20:01:07 +00:00
|
|
|
return left.Node.Node < right.Node.Node
|
2020-11-20 15:23:35 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-10-03 00:04:45 +00:00
|
|
|
// Result returns the structs.IndexedCheckServiceNodes stored by this view.
|
2022-07-12 10:37:48 +00:00
|
|
|
func (s *HealthView) Result(index uint64) interface{} {
|
2020-10-03 00:04:45 +00:00
|
|
|
result := structs.IndexedCheckServiceNodes{
|
|
|
|
Nodes: make(structs.CheckServiceNodes, 0, len(s.state)),
|
|
|
|
QueryMeta: structs.QueryMeta{
|
2021-06-28 20:48:10 +00:00
|
|
|
Index: index,
|
|
|
|
Backend: structs.QueryBackendStreaming,
|
2020-10-03 00:04:45 +00:00
|
|
|
},
|
|
|
|
}
|
2020-09-18 22:25:56 +00:00
|
|
|
for _, node := range s.state {
|
|
|
|
result.Nodes = append(result.Nodes, node)
|
|
|
|
}
|
2020-11-25 20:01:07 +00:00
|
|
|
sortCheckServiceNodes(&result)
|
2020-11-20 15:23:35 +00:00
|
|
|
|
2021-02-23 19:27:24 +00:00
|
|
|
return &result
|
2020-09-18 22:25:56 +00:00
|
|
|
}
|
2020-09-29 21:42:48 +00:00
|
|
|
|
2022-07-12 10:37:48 +00:00
|
|
|
func (s *HealthView) Reset() {
|
2020-09-29 21:42:48 +00:00
|
|
|
s.state = make(map[string]structs.CheckServiceNode)
|
|
|
|
}
|
2021-02-08 23:54:37 +00:00
|
|
|
|
|
|
|
// serviceTagEvaluator implements the filterEvaluator to perform filtering
|
|
|
|
// by service tags. bexpr can not be used at this time, because the filtering
|
|
|
|
// must be case insensitive for backwards compatibility. In the future this
|
|
|
|
// may be replaced with bexpr once case insensitive support is added.
|
|
|
|
type serviceTagEvaluator struct {
|
|
|
|
tags []string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m serviceTagEvaluator) Evaluate(data interface{}) (bool, error) {
|
|
|
|
csn, ok := data.(structs.CheckServiceNode)
|
|
|
|
if !ok {
|
|
|
|
return false, fmt.Errorf("unexpected type %T for structs.CheckServiceNode filter", data)
|
|
|
|
}
|
|
|
|
for _, tag := range m.tags {
|
|
|
|
if !serviceHasTag(csn.Service, tag) {
|
|
|
|
// If any one of the expected tags was not found, filter the service
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func serviceHasTag(sn *structs.NodeService, tag string) bool {
|
|
|
|
for _, t := range sn.Tags {
|
|
|
|
if strings.EqualFold(t, tag) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|