open-consul/agent/grpc-external/services/peerstream/subscription_view.go

169 lines
5.0 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package peerstream
import (
"fmt"
"sort"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/acl/resolver"
"github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/agent/consul/stream"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/submatview"
"github.com/hashicorp/consul/proto/private/pbservice"
"github.com/hashicorp/consul/proto/private/pbsubscribe"
)
type Subscriber interface {
Subscribe(req *stream.SubscribeRequest) (*stream.Subscription, error)
}
type exportedServiceRequest struct {
logger hclog.Logger
req structs.ServiceSpecificRequest
sub Subscriber
}
func newExportedStandardServiceRequest(logger hclog.Logger, svc structs.ServiceName, sub Subscriber) *exportedServiceRequest {
req := structs.ServiceSpecificRequest{
ServiceName: svc.Name,
Connect: false,
EnterpriseMeta: svc.EnterpriseMeta,
}
return &exportedServiceRequest{
logger: logger,
req: req,
sub: sub,
}
}
// CacheInfo implements submatview.Request
func (e *exportedServiceRequest) CacheInfo() cache.RequestInfo {
return e.req.CacheInfo()
}
func (e *exportedServiceRequest) getTopic() pbsubscribe.Topic {
if e.req.Connect {
return pbsubscribe.Topic_ServiceHealthConnect
}
// Using the Topic_ServiceHealth will ignore proxies unless the ServiceName is a proxy name.
return pbsubscribe.Topic_ServiceHealth
}
// NewMaterializer implements submatview.Request
func (e *exportedServiceRequest) NewMaterializer() (submatview.Materializer, error) {
// TODO(peering): reinstate this
// if e.req.Connect {
// return nil, fmt.Errorf("connect views are not supported")
// }
reqFn := func(index uint64) *pbsubscribe.SubscribeRequest {
return &pbsubscribe.SubscribeRequest{
Topic: e.getTopic(),
Subject: &pbsubscribe.SubscribeRequest_NamedSubject{
NamedSubject: &pbsubscribe.NamedSubject{
Key: e.req.ServiceName,
Namespace: e.req.EnterpriseMeta.NamespaceOrEmpty(),
Partition: e.req.EnterpriseMeta.PartitionOrEmpty(),
},
},
Token: e.req.Token,
Datacenter: e.req.Datacenter,
Index: index,
}
}
deps := submatview.LocalMaterializerDeps{
Backend: e.sub,
ACLResolver: DANGER_NO_AUTH{},
Deps: submatview.Deps{
View: newExportedServicesView(),
Logger: e.logger,
Request: reqFn,
},
}
return submatview.NewLocalMaterializer(deps), nil
}
// DANGER_NO_AUTH implements submatview.ACLResolver to short-circuit authorization
// in cases where it is handled somewhere else (e.g. in an RPC handler).
type DANGER_NO_AUTH struct{}
func (DANGER_NO_AUTH) ResolveTokenAndDefaultMeta(string, *acl.EnterpriseMeta, *acl.AuthorizerContext) (resolver.Result, error) {
return resolver.Result{Authorizer: acl.ManageAll()}, nil
}
// Type implements submatview.Request
func (e *exportedServiceRequest) Type() string {
return "leader.peering.stream.exportedServiceRequest"
}
// exportedServicesView implements submatview.View for storing the view state
// of an exported service's 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.
//
// Unlike rpcclient.healthView, there is no need for a filter because for exported services
// we export all instances unconditionally.
type exportedServicesView struct {
state map[string]*pbservice.CheckServiceNode
}
func newExportedServicesView() *exportedServicesView {
return &exportedServicesView{
state: make(map[string]*pbservice.CheckServiceNode),
}
}
// Reset implements submatview.View
func (s *exportedServicesView) Reset() {
s.state = make(map[string]*pbservice.CheckServiceNode)
}
// Update implements submatview.View
func (s *exportedServicesView) Update(events []*pbsubscribe.Event) error {
for _, event := range events {
serviceHealth := event.GetServiceHealth()
if serviceHealth == nil {
return fmt.Errorf("unexpected event type for service health view: %T",
event.GetPayload())
}
id := serviceHealth.CheckServiceNode.UniqueID()
switch serviceHealth.Op {
case pbsubscribe.CatalogOp_Register:
s.state[id] = serviceHealth.CheckServiceNode
case pbsubscribe.CatalogOp_Deregister:
delete(s.state, id)
}
}
return nil
}
// Result returns the CheckServiceNodes stored by this view.
// Result implements submatview.View
func (s *exportedServicesView) Result(index uint64) interface{} {
result := pbservice.IndexedCheckServiceNodes{
Nodes: make([]*pbservice.CheckServiceNode, 0, len(s.state)),
Index: index,
}
for _, node := range s.state {
result.Nodes = append(result.Nodes, node)
}
sortCheckServiceNodes(&result)
return &result
}
// sortCheckServiceNodes stable sorts the results to match memdb semantics.
func sortCheckServiceNodes(n *pbservice.IndexedCheckServiceNodes) {
sort.SliceStable(n.Nodes, func(i, j int) bool {
return n.Nodes[i].UniqueID() < n.Nodes[j].UniqueID()
})
}