NET-5879 - expose sameness group param on service health endpoint and move sameness group health fallback logic into HealthService RPC layer (#21096)

* NET-5879 - move the filter for non-passing to occur in the health RPC layer rather than the callers of the RPC

* fix import of slices

* NET-5879 - expose sameness group param on service health endpoint and move sameness group health fallback logic into HealthService RPC layer

* fixing deepcopy

* fix license headers
pull/21107/head
John Murret 6 months ago committed by GitHub
parent a975b04302
commit 9b2c1be053
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -8,15 +8,15 @@ import (
"sort" "sort"
"github.com/armon/go-metrics" "github.com/armon/go-metrics"
bexpr "github.com/hashicorp/go-bexpr"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
hashstructure_v2 "github.com/mitchellh/hashstructure/v2" hashstructure_v2 "github.com/mitchellh/hashstructure/v2"
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/configentry" "github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb"
) )
// Health endpoint is used to query the health information // Health endpoint is used to query the health information
@ -250,13 +250,19 @@ func (h *Health) ServiceNodes(args *structs.ServiceSpecificRequest, reply *struc
func(ws memdb.WatchSet, state *state.Store) error { func(ws memdb.WatchSet, state *state.Store) error {
var thisReply structs.IndexedCheckServiceNodes var thisReply structs.IndexedCheckServiceNodes
index, nodes, err := f(ws, state, args) sgIdx, sgArgs, err := h.getArgsForSamenessGroupMembers(args, ws, state)
if err != nil {
return err
}
for _, arg := range sgArgs {
index, nodes, err := f(ws, state, arg)
if err != nil { if err != nil {
return err return err
} }
resolvedNodes := nodes resolvedNodes := nodes
if args.MergeCentralConfig { if arg.MergeCentralConfig {
for _, node := range resolvedNodes { for _, node := range resolvedNodes {
ns := node.Service ns := node.Service
if ns.IsSidecarProxy() || ns.IsGateway() { if ns.IsSidecarProxy() || ns.IsGateway() {
@ -293,8 +299,8 @@ func (h *Health) ServiceNodes(args *structs.ServiceSpecificRequest, reply *struc
thisReply.Index, thisReply.Nodes = index, resolvedNodes thisReply.Index, thisReply.Nodes = index, resolvedNodes
if len(args.NodeMetaFilters) > 0 { if len(arg.NodeMetaFilters) > 0 {
thisReply.Nodes = nodeMetaFilter(args.NodeMetaFilters, thisReply.Nodes) thisReply.Nodes = nodeMetaFilter(arg.NodeMetaFilters, thisReply.Nodes)
} }
raw, err := filter.Execute(thisReply.Nodes) raw, err := filter.Execute(thisReply.Nodes)
@ -302,18 +308,29 @@ func (h *Health) ServiceNodes(args *structs.ServiceSpecificRequest, reply *struc
return err return err
} }
filteredNodes := raw.(structs.CheckServiceNodes) filteredNodes := raw.(structs.CheckServiceNodes)
thisReply.Nodes = filteredNodes.Filter(structs.CheckServiceNodeFilterOptions{FilterType: args.HealthFilterType}) thisReply.Nodes = filteredNodes.Filter(structs.CheckServiceNodeFilterOptions{FilterType: arg.HealthFilterType})
// Note: we filter the results with ACLs *after* applying the user-supplied // Note: we filter the results with ACLs *after* applying the user-supplied
// bexpr filter, to ensure QueryMeta.ResultsFilteredByACLs does not include // bexpr filter, to ensure QueryMeta.ResultsFilteredByACLs does not include
// results that would be filtered out even if the user did have permission. // results that would be filtered out even if the user did have permission.
if err := h.srv.filterACL(args.Token, &thisReply); err != nil { if err := h.srv.filterACL(arg.Token, &thisReply); err != nil {
return err return err
} }
if err := h.srv.sortNodesByDistanceFrom(args.Source, thisReply.Nodes); err != nil { if err := h.srv.sortNodesByDistanceFrom(arg.Source, thisReply.Nodes); err != nil {
return err return err
} }
if len(thisReply.Nodes) > 0 {
break
}
}
// If sameness group was used, evaluate the index of the sameness group
// and update the index of the response if it is greater. If sameness group is not
// used, the sgIdx will be 0 in this evaluation.
if sgIdx > thisReply.Index {
thisReply.Index = sgIdx
}
*reply = thisReply *reply = thisReply
return nil return nil

@ -0,0 +1,38 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !consulent
package consul
import (
"errors"
"github.com/hashicorp/go-memdb"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
)
// getArgsForSamenessGroupMembers returns the arguments for the sameness group members if SamenessGroup
// field is set in the ServiceSpecificRequest. It returns the index of the sameness group, the arguments
// for the sameness group members and an error if any.
// If SamenessGroup is not set, it returns:
// - the index 0
// - an array containing the original arguments
// - nil error
// If SamenessGroup is set on CE, it returns::
// - the index of 0
// - nil array
// - an error indicating that sameness groups are not supported in consul CE
// If SamenessGroup is set on ENT, it returns:
// - the index of the sameness group
// - an array containing the arguments for the sameness group members
// - nil error
func (h *Health) getArgsForSamenessGroupMembers(args *structs.ServiceSpecificRequest,
ws memdb.WatchSet, state *state.Store) (uint64, []*structs.ServiceSpecificRequest, error) {
if args.SamenessGroup != "" {
return 0, nil, errors.New("sameness groups are not supported in consul CE")
}
return 0, []*structs.ServiceSpecificRequest{args}, nil
}

@ -5,7 +5,6 @@ package discovery
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"strings" "strings"
@ -469,7 +468,7 @@ func (f *V1DataFetcher) buildResultsFromServiceNodes(nodes []structs.CheckServic
Namespace: n.Service.NamespaceOrEmpty(), Namespace: n.Service.NamespaceOrEmpty(),
Partition: n.Service.PartitionOrEmpty(), Partition: n.Service.PartitionOrEmpty(),
Datacenter: n.Node.Datacenter, Datacenter: n.Node.Datacenter,
PeerName: req.Tenancy.Peer, PeerName: n.Service.PeerName,
}, },
}) })
} }
@ -542,23 +541,10 @@ RPC:
return &out, nil return &out, nil
} }
// fetchService is used to look up a service in the Consul catalog.
func (f *V1DataFetcher) fetchService(ctx Context, req *QueryPayload, func (f *V1DataFetcher) fetchService(ctx Context, req *QueryPayload,
cfg *V1DataFetcherDynamicConfig, lookupType LookupType) ([]*Result, error) { cfg *V1DataFetcherDynamicConfig, lookupType LookupType) ([]*Result, error) {
f.logger.Trace("fetchService", "req", req) f.logger.Trace(fmt.Sprintf("fetchService - req: %+v", req))
if req.Tenancy.SamenessGroup == "" {
return f.fetchServiceBasedOnTenancy(ctx, req, cfg, lookupType)
}
return f.fetchServiceFromSamenessGroup(ctx, req, cfg, lookupType)
}
// fetchServiceBasedOnTenancy is used to look up a service in the Consul catalog based on its tenancy or default tenancy.
func (f *V1DataFetcher) fetchServiceBasedOnTenancy(ctx Context, req *QueryPayload,
cfg *V1DataFetcherDynamicConfig, lookupType LookupType) ([]*Result, error) {
f.logger.Trace(fmt.Sprintf("fetchServiceBasedOnTenancy - req: %+v", req))
if req.Tenancy.SamenessGroup != "" {
return nil, errors.New("sameness groups are not allowed for service lookups based on tenancy")
}
// If no datacenter is passed, default to our own // If no datacenter is passed, default to our own
datacenter := cfg.Datacenter datacenter := cfg.Datacenter
@ -573,14 +559,13 @@ func (f *V1DataFetcher) fetchServiceBasedOnTenancy(ctx Context, req *QueryPayloa
if req.Tag != "" { if req.Tag != "" {
serviceTags = []string{req.Tag} serviceTags = []string{req.Tag}
} }
healthFilterType := structs.HealthFilterExcludeCritical healthFilterType := structs.HealthFilterExcludeCritical
if cfg.OnlyPassing { if cfg.OnlyPassing {
healthFilterType = structs.HealthFilterIncludeOnlyPassing healthFilterType = structs.HealthFilterIncludeOnlyPassing
} }
args := structs.ServiceSpecificRequest{ args := structs.ServiceSpecificRequest{
PeerName: req.Tenancy.Peer, PeerName: req.Tenancy.Peer,
SamenessGroup: req.Tenancy.SamenessGroup,
Connect: lookupType == LookupTypeConnect, Connect: lookupType == LookupTypeConnect,
Ingress: lookupType == LookupTypeIngress, Ingress: lookupType == LookupTypeIngress,
Datacenter: datacenter, Datacenter: datacenter,
@ -611,11 +596,6 @@ func (f *V1DataFetcher) fetchServiceBasedOnTenancy(ctx Context, req *QueryPayloa
return nil, ErrNotFound return nil, ErrNotFound
} }
// If we have no nodes, return not found!
if len(out.Nodes) == 0 {
return nil, ErrNotFound
}
// Perform a random shuffle // Perform a random shuffle
out.Nodes.Shuffle() out.Nodes.Shuffle()
return f.buildResultsFromServiceNodes(out.Nodes, req, nil), nil return f.buildResultsFromServiceNodes(out.Nodes, req, nil), nil

@ -6,9 +6,6 @@
package discovery package discovery
import ( import (
"errors"
"fmt"
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
) )
@ -27,12 +24,3 @@ func validateEnterpriseTenancy(req QueryTenancy) error {
func queryTenancyToEntMeta(_ QueryTenancy) acl.EnterpriseMeta { func queryTenancyToEntMeta(_ QueryTenancy) acl.EnterpriseMeta {
return acl.EnterpriseMeta{} return acl.EnterpriseMeta{}
} }
// fetchServiceFromSamenessGroup fetches a service from a sameness group.
func (f *V1DataFetcher) fetchServiceFromSamenessGroup(ctx Context, req *QueryPayload, cfg *V1DataFetcherDynamicConfig, lookupType LookupType) ([]*Result, error) {
f.logger.Trace(fmt.Sprintf("fetchServiceFromSamenessGroup - req: %+v", req))
if req.Tenancy.SamenessGroup == "" {
return nil, errors.New("sameness groups must be provided for service lookups")
}
return f.fetchServiceBasedOnTenancy(ctx, req, cfg, lookupType)
}

@ -184,6 +184,7 @@ func Test_FetchEndpoints(t *testing.T) {
Service: &structs.NodeService{ Service: &structs.NodeService{
Address: "service-address", Address: "service-address",
Service: "service-name", Service: "service-name",
PeerName: "test-peer",
}, },
}, },
}, },

@ -92,6 +92,7 @@ type serviceLookup struct {
PeerName string PeerName string
Datacenter string Datacenter string
Service string Service string
SamenessGroup string
Tag string Tag string
MaxRecursionLevel int MaxRecursionLevel int
Connect bool Connect bool
@ -439,18 +440,11 @@ func (d *DNSServer) handlePtr(resp dns.ResponseWriter, req *dns.Msg) {
// server side to avoid transferring the entire node list. // server side to avoid transferring the entire node list.
if err := d.agent.RPC(context.Background(), "Catalog.ListNodes", &args, &out); err == nil { if err := d.agent.RPC(context.Background(), "Catalog.ListNodes", &args, &out); err == nil {
for _, n := range out.Nodes { for _, n := range out.Nodes {
lookup := serviceLookup{
// Peering PTR lookups are currently not supported, so we don't
// need to populate that field for creating the node FQDN.
// PeerName: n.PeerName,
Datacenter: n.Datacenter,
EnterpriseMeta: *n.GetEnterpriseMeta(),
}
arpa, _ := dns.ReverseAddr(n.Address) arpa, _ := dns.ReverseAddr(n.Address)
if arpa == qName { if arpa == qName {
ptr := &dns.PTR{ ptr := &dns.PTR{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: 0}, Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: 0},
Ptr: nodeCanonicalDNSName(lookup, n.Node, d.domain), Ptr: nodeCanonicalDNSName(n, d.domain),
} }
m.Answer = append(m.Answer, ptr) m.Answer = append(m.Answer, ptr)
break break
@ -738,6 +732,10 @@ type queryLocality struct {
// not be shared between datacenters. In all other cases, it should be considered a DC. // not be shared between datacenters. In all other cases, it should be considered a DC.
peerOrDatacenter string peerOrDatacenter string
// samenessGroup is the samenessGroup name parsed from a label that has explicit parts.
// Example query: <service>.service.<sameness group>.sg.consul
samenessGroup string
acl.EnterpriseMeta acl.EnterpriseMeta
} }
@ -805,23 +803,21 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi
return invalid() return invalid()
} }
localities, err := d.parseSamenessGroupLocality(cfg, querySuffixes, invalid) locality, err := d.parseSamenessGroupLocality(cfg, querySuffixes, invalid)
if err != nil { if err != nil {
return err return err
} }
// Loop over the localities and return as soon as a lookup is successful
for _, locality := range localities {
d.logger.Debug("labels", "querySuffixes", querySuffixes)
lookup := serviceLookup{ lookup := serviceLookup{
Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter), Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter),
PeerName: locality.peer, PeerName: locality.peer,
SamenessGroup: locality.samenessGroup,
Connect: false, Connect: false,
Ingress: false, Ingress: false,
MaxRecursionLevel: maxRecursionLevel, MaxRecursionLevel: maxRecursionLevel,
EnterpriseMeta: locality.EnterpriseMeta, EnterpriseMeta: locality.EnterpriseMeta,
} }
// Only one of dc or peer can be used. // Only one of dc or peer can be used.
if lookup.PeerName != "" { if lookup.PeerName != "" {
lookup.Datacenter = "" lookup.Datacenter = ""
@ -858,7 +854,6 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi
if err == nil { if err == nil {
return nil return nil
} }
}
// We've exhausted all DNS possibilities so return here // We've exhausted all DNS possibilities so return here
return err return err
@ -1456,6 +1451,7 @@ func (d *DNSServer) lookupServiceNodes(cfg *dnsConfig, lookup serviceLookup) (st
} }
args := structs.ServiceSpecificRequest{ args := structs.ServiceSpecificRequest{
PeerName: lookup.PeerName, PeerName: lookup.PeerName,
SamenessGroup: lookup.SamenessGroup,
Connect: lookup.Connect, Connect: lookup.Connect,
Ingress: lookup.Ingress, Ingress: lookup.Ingress,
Datacenter: lookup.Datacenter, Datacenter: lookup.Datacenter,
@ -1758,20 +1754,20 @@ func findWeight(node structs.CheckServiceNode) int {
} }
} }
func (d *DNSServer) encodeIPAsFqdn(questionName string, lookup serviceLookup, ip net.IP) string { func (d *DNSServer) encodeIPAsFqdn(questionName string, serviceNode structs.CheckServiceNode, ip net.IP) string {
ipv4 := ip.To4() ipv4 := ip.To4()
respDomain := d.getResponseDomain(questionName) respDomain := d.getResponseDomain(questionName)
ipStr := hex.EncodeToString(ip) ipStr := hex.EncodeToString(ip)
if ipv4 != nil { if ipv4 != nil {
ipStr = ipStr[len(ipStr)-(net.IPv4len*2):] ipStr = ipStr[len(ipStr)-(net.IPv4len*2):]
} }
if lookup.PeerName != "" { if serviceNode.Service.PeerName != "" {
// Exclude the datacenter from the FQDN on the addr for peers. // Exclude the datacenter from the FQDN on the addr for peers.
// This technically makes no difference, since the addr endpoint ignores the DC // This technically makes no difference, since the addr endpoint ignores the DC
// component of the request, but do it anyway for a less confusing experience. // component of the request, but do it anyway for a less confusing experience.
return fmt.Sprintf("%s.addr.%s", ipStr, respDomain) return fmt.Sprintf("%s.addr.%s", ipStr, respDomain)
} }
return fmt.Sprintf("%s.addr.%s.%s", ipStr, lookup.Datacenter, respDomain) return fmt.Sprintf("%s.addr.%s.%s", ipStr, serviceNode.Node.Datacenter, respDomain)
} }
// Craft dns records for a an A record for an IP address // Craft dns records for a an A record for an IP address
@ -1860,7 +1856,7 @@ func (d *DNSServer) makeRecordFromServiceNode(lookup serviceLookup, serviceNode
if q.Qtype == dns.TypeSRV { if q.Qtype == dns.TypeSRV {
respDomain := d.getResponseDomain(q.Name) respDomain := d.getResponseDomain(q.Name)
nodeFQDN := nodeCanonicalDNSName(lookup, serviceNode.Node.Node, respDomain) nodeFQDN := nodeCanonicalDNSName(serviceNode.Node, respDomain)
answers := []dns.RR{ answers := []dns.RR{
&dns.SRV{ &dns.SRV{
Hdr: dns.RR_Header{ Hdr: dns.RR_Header{
@ -1895,7 +1891,7 @@ func (d *DNSServer) makeRecordFromIP(lookup serviceLookup, addr net.IP, serviceN
} }
if q.Qtype == dns.TypeSRV { if q.Qtype == dns.TypeSRV {
ipFQDN := d.encodeIPAsFqdn(q.Name, lookup, addr) ipFQDN := d.encodeIPAsFqdn(q.Name, serviceNode, addr)
answers := []dns.RR{ answers := []dns.RR{
&dns.SRV{ &dns.SRV{
Hdr: dns.RR_Header{ Hdr: dns.RR_Header{
@ -2076,7 +2072,7 @@ func (d *DNSServer) addServiceSRVRecordsToMessage(cfg *dnsConfig, lookup service
resp.Extra = append(resp.Extra, extra...) resp.Extra = append(resp.Extra, extra...)
if cfg.NodeMetaTXT { if cfg.NodeMetaTXT {
resp.Extra = append(resp.Extra, d.makeTXTRecordFromNodeMeta(nodeCanonicalDNSName(lookup, node.Node.Node, respDomain), node.Node, ttl)...) resp.Extra = append(resp.Extra, d.makeTXTRecordFromNodeMeta(nodeCanonicalDNSName(node.Node, respDomain), node.Node, ttl)...)
} }
} }
} }

@ -10,6 +10,7 @@ import (
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/structs"
) )
// NOTE: these functions have also been copied to agent/dns package for dns v2. // NOTE: these functions have also been copied to agent/dns package for dns v2.
@ -63,27 +64,27 @@ func (d *DNSServer) parseLocality(labels []string, cfg *dnsConfig) (queryLocalit
type querySameness struct{} type querySameness struct{}
// parseSamenessGroupLocality wraps parseLocality in CE // parseSamenessGroupLocality wraps parseLocality in CE
func (d *DNSServer) parseSamenessGroupLocality(cfg *dnsConfig, labels []string, errfnc func() error) ([]queryLocality, error) { func (d *DNSServer) parseSamenessGroupLocality(cfg *dnsConfig, labels []string, errfnc func() error) (queryLocality, error) {
locality, ok := d.parseLocality(labels, cfg) locality, ok := d.parseLocality(labels, cfg)
if !ok { if !ok {
return nil, errfnc() return queryLocality{}, errfnc()
} }
return []queryLocality{locality}, nil return locality, nil
} }
func serviceCanonicalDNSName(name, kind, datacenter, domain string, _ *acl.EnterpriseMeta) string { func serviceCanonicalDNSName(name, kind, datacenter, domain string, _ *acl.EnterpriseMeta) string {
return fmt.Sprintf("%s.%s.%s.%s", name, kind, datacenter, domain) return fmt.Sprintf("%s.%s.%s.%s", name, kind, datacenter, domain)
} }
func nodeCanonicalDNSName(lookup serviceLookup, nodeName, respDomain string) string { func nodeCanonicalDNSName(node *structs.Node, respDomain string) string {
if lookup.PeerName != "" { if node.PeerName != "" {
// We must return a more-specific DNS name for peering so // We must return a more-specific DNS name for peering so
// that there is no ambiguity with lookups. // that there is no ambiguity with lookups.
return fmt.Sprintf("%s.node.%s.peer.%s", return fmt.Sprintf("%s.node.%s.peer.%s",
nodeName, node.Node,
lookup.PeerName, node.PeerName,
respDomain) respDomain)
} }
// Return a simpler format for non-peering nodes. // Return a simpler format for non-peering nodes.
return fmt.Sprintf("%s.node.%s.%s", nodeName, lookup.Datacenter, respDomain) return fmt.Sprintf("%s.node.%s.%s", node.Node, node.Datacenter, respDomain)
} }

@ -188,6 +188,10 @@ func (s *HTTPHandlers) healthServiceNodes(resp http.ResponseWriter, req *http.Re
} }
s.parsePeerName(req, &args) s.parsePeerName(req, &args)
s.parseSamenessGroup(req, &args)
if args.SamenessGroup != "" && args.PeerName != "" {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "peer-name and sameness-group are mutually exclusive"}
}
// Check for tags // Check for tags
params := req.URL.Query() params := req.URL.Query()
@ -214,7 +218,7 @@ func (s *HTTPHandlers) healthServiceNodes(resp http.ResponseWriter, req *http.Re
prefix = "/v1/health/service/" prefix = "/v1/health/service/"
} }
// Parse out the service name from the query params // Parse the service name from the query params
args.ServiceName = strings.TrimPrefix(req.URL.Path, prefix) args.ServiceName = strings.TrimPrefix(req.URL.Path, prefix)
if args.ServiceName == "" { if args.ServiceName == "" {
return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing service name"} return nil, HTTPError{StatusCode: http.StatusBadRequest, Reason: "Missing service name"}

@ -0,0 +1,30 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !consulent
package agent
import (
"github.com/hashicorp/consul/testrpc"
"github.com/stretchr/testify/require"
"net/http"
"net/http/httptest"
"testing"
)
func TestHealthServiceNodes_SamenessGroup_ErrorsOnCE(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
a := NewTestAgent(t, "")
defer a.Shutdown()
testrpc.WaitForTestAgent(t, a.RPC, "dc1")
req, _ := http.NewRequest("GET", "/v1/health/service/consul?dc=dc1&sameness-group=foo", nil)
resp := httptest.NewRecorder()
_, err := a.srv.HealthServiceNodes(resp, req)
require.ErrorContains(t, err, "sameness groups are not supported in consul CE")
}

@ -739,7 +739,7 @@ func decodeBody(body io.Reader, out interface{}) error {
return lib.DecodeJSON(body, out) return lib.DecodeJSON(body, out)
} }
// decodeBodyDeprecated is deprecated, please ues decodeBody above. // decodeBodyDeprecated is deprecated, please use decodeBody above.
// decodeBodyDeprecated is used to decode a JSON request body // decodeBodyDeprecated is used to decode a JSON request body
func decodeBodyDeprecated(req *http.Request, out interface{}, cb func(interface{}) error) error { func decodeBodyDeprecated(req *http.Request, out interface{}, cb func(interface{}) error) error {
// This generally only happens in tests since real HTTP requests set // This generally only happens in tests since real HTTP requests set
@ -1208,6 +1208,15 @@ func (s *HTTPHandlers) parsePeerName(req *http.Request, args *structs.ServiceSpe
} }
} }
func (s *HTTPHandlers) parseSamenessGroup(req *http.Request, args *structs.ServiceSpecificRequest) {
if sg := req.URL.Query().Get("sg"); sg != "" {
args.SamenessGroup = sg
}
if sg := req.URL.Query().Get("sameness-group"); sg != "" {
args.SamenessGroup = sg
}
}
// parseMetaFilter is used to parse the ?node-meta=key:value query parameter, used for // parseMetaFilter is used to parse the ?node-meta=key:value query parameter, used for
// filtering results to nodes with the given metadata key/value // filtering results to nodes with the given metadata key/value
func (s *HTTPHandlers) parseMetaFilter(req *http.Request) map[string]string { func (s *HTTPHandlers) parseMetaFilter(req *http.Request) map[string]string {

@ -105,7 +105,12 @@ func (c *Client) useStreaming(req structs.ServiceSpecificRequest) bool {
// Streaming is incompatible with NearestN queries (due to lack of ordering), // Streaming is incompatible with NearestN queries (due to lack of ordering),
// so we can only use it if the NearestN would never work (Node == "") // so we can only use it if the NearestN would never work (Node == "")
// or if we explicitly say to ignore the Node field for queries (agentless xDS). // or if we explicitly say to ignore the Node field for queries (agentless xDS).
(req.Source.Node == "" || req.Source.DisableNode) (req.Source.Node == "" || req.Source.DisableNode) &&
// Streaming is incompatible with SamenessGroup queries at the moment because
// the subscribe functionality maps to queries based on the service name and tenancy information
// it does not support the ability to subscribe to the same service in different partitions or peers
// and materialize the results into a single view with the first healthy sameness group member.
req.SamenessGroup == ""
} }
func (c *Client) newServiceRequest(req structs.ServiceSpecificRequest) serviceRequest { func (c *Client) newServiceRequest(req structs.ServiceSpecificRequest) serviceRequest {

@ -98,6 +98,17 @@ func TestClient_ServiceNodes_BackendRouting(t *testing.T) {
}, },
expected: useRPC, expected: useRPC,
}, },
{
name: "rpc if sameness group",
req: structs.ServiceSpecificRequest{
Datacenter: "dc1",
ServiceName: "web1",
SamenessGroup: "sg1",
MergeCentralConfig: false,
QueryOptions: structs.QueryOptions{MinQueryIndex: 22},
},
expected: useRPC,
},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -246,6 +257,15 @@ func TestClient_Notify_BackendRouting(t *testing.T) {
}, },
expected: useCache, expected: useCache,
}, },
{
name: "use cache for sameness group request",
req: structs.ServiceSpecificRequest{
Datacenter: "dc1",
ServiceName: "web1",
SamenessGroup: "test-group",
},
expected: useCache,
},
} }
for _, tc := range testCases { for _, tc := range testCases {

@ -52,6 +52,7 @@ deep-copy \
-type ServiceRoute \ -type ServiceRoute \
-type ServiceRouteDestination \ -type ServiceRouteDestination \
-type ServiceRouteMatch \ -type ServiceRouteMatch \
-type ServiceSpecificRequest \
-type TCPRouteConfigEntry \ -type TCPRouteConfigEntry \
-type Upstream \ -type Upstream \
-type UpstreamConfiguration \ -type UpstreamConfiguration \

@ -1,4 +1,4 @@
// generated by deep-copy -pointer-receiver -o ./structs.deepcopy.go -type APIGatewayListener -type BoundAPIGatewayListener -type CARoot -type CheckServiceNode -type CheckType -type CompiledDiscoveryChain -type ConnectProxyConfig -type DiscoveryFailover -type DiscoveryGraphNode -type DiscoveryResolver -type DiscoveryRoute -type DiscoverySplit -type ExposeConfig -type ExportedServicesConfigEntry -type FileSystemCertificateConfigEntry -type GatewayService -type GatewayServiceTLSConfig -type HTTPHeaderModifiers -type HTTPRouteConfigEntry -type HashPolicy -type HealthCheck -type IndexedCARoots -type IngressListener -type InlineCertificateConfigEntry -type Intention -type IntentionPermission -type LoadBalancer -type MeshConfigEntry -type MeshDirectionalTLSConfig -type MeshTLSConfig -type Node -type NodeService -type PeeringServiceMeta -type ServiceConfigEntry -type ServiceConfigResponse -type ServiceConnect -type ServiceDefinition -type ServiceResolverConfigEntry -type ServiceResolverFailover -type ServiceRoute -type ServiceRouteDestination -type ServiceRouteMatch -type TCPRouteConfigEntry -type Upstream -type UpstreamConfiguration -type Status -type BoundAPIGatewayConfigEntry ./; DO NOT EDIT. // generated by deep-copy -pointer-receiver -o ./structs.deepcopy.go -type APIGatewayListener -type BoundAPIGatewayListener -type CARoot -type CheckServiceNode -type CheckType -type CompiledDiscoveryChain -type ConnectProxyConfig -type DiscoveryFailover -type DiscoveryGraphNode -type DiscoveryResolver -type DiscoveryRoute -type DiscoverySplit -type ExposeConfig -type ExportedServicesConfigEntry -type FileSystemCertificateConfigEntry -type GatewayService -type GatewayServiceTLSConfig -type HTTPHeaderModifiers -type HTTPRouteConfigEntry -type HashPolicy -type HealthCheck -type IndexedCARoots -type IngressListener -type InlineCertificateConfigEntry -type Intention -type IntentionPermission -type LoadBalancer -type MeshConfigEntry -type MeshDirectionalTLSConfig -type MeshTLSConfig -type Node -type NodeService -type PeeringServiceMeta -type ServiceConfigEntry -type ServiceConfigResponse -type ServiceConnect -type ServiceDefinition -type ServiceResolverConfigEntry -type ServiceResolverFailover -type ServiceRoute -type ServiceRouteDestination -type ServiceRouteMatch -type ServiceSpecificRequest -type TCPRouteConfigEntry -type Upstream -type UpstreamConfiguration -type Status -type BoundAPIGatewayConfigEntry ./; DO NOT EDIT.
package structs package structs
@ -1197,6 +1197,22 @@ func (o *ServiceRouteMatch) DeepCopy() *ServiceRouteMatch {
return &cp return &cp
} }
// DeepCopy generates a deep copy of *ServiceSpecificRequest
func (o *ServiceSpecificRequest) DeepCopy() *ServiceSpecificRequest {
var cp ServiceSpecificRequest = *o
if o.NodeMetaFilters != nil {
cp.NodeMetaFilters = make(map[string]string, len(o.NodeMetaFilters))
for k2, v2 := range o.NodeMetaFilters {
cp.NodeMetaFilters[k2] = v2
}
}
if o.ServiceTags != nil {
cp.ServiceTags = make([]string, len(o.ServiceTags))
copy(cp.ServiceTags, o.ServiceTags)
}
return &cp
}
// DeepCopy generates a deep copy of *TCPRouteConfigEntry // DeepCopy generates a deep copy of *TCPRouteConfigEntry
func (o *TCPRouteConfigEntry) DeepCopy() *TCPRouteConfigEntry { func (o *TCPRouteConfigEntry) DeepCopy() *TCPRouteConfigEntry {
var cp TCPRouteConfigEntry = *o var cp TCPRouteConfigEntry = *o

@ -753,6 +753,9 @@ type ServiceSpecificRequest struct {
// The name of the peer that the requested service was imported from. // The name of the peer that the requested service was imported from.
PeerName string PeerName string
// The name of the sameness group that should be the target of the query.
SamenessGroup string
NodeMetaFilters map[string]string NodeMetaFilters map[string]string
ServiceName string ServiceName string
ServiceKind ServiceKind ServiceKind ServiceKind
@ -821,6 +824,7 @@ func (r *ServiceSpecificRequest) CacheInfo() cache.RequestInfo {
r.Filter, r.Filter,
r.EnterpriseMeta, r.EnterpriseMeta,
r.PeerName, r.PeerName,
r.SamenessGroup,
r.Ingress, r.Ingress,
r.ServiceKind, r.ServiceKind,
r.MergeCentralConfig, r.MergeCentralConfig,

@ -117,6 +117,13 @@ type QueryOptions struct {
// Note: Partitions are available only in Consul Enterprise // Note: Partitions are available only in Consul Enterprise
Partition string Partition string
// SamenessGroup is used find the SamenessGroup in the given
// Partition and will find the failover order for the Service
// from the SamenessGroup Members, with the given Partition being
// the first member.
// Note: SamenessGroups are available only in Consul Enterprise
SamenessGroup string
// Providing a datacenter overwrites the DC provided // Providing a datacenter overwrites the DC provided
// by the Config // by the Config
Datacenter string Datacenter string
@ -847,6 +854,12 @@ func (r *request) setQueryOptions(q *QueryOptions) {
// rather than the alternative short-hand "ap" // rather than the alternative short-hand "ap"
r.params.Set("partition", q.Partition) r.params.Set("partition", q.Partition)
} }
if q.SamenessGroup != "" {
// For backwards-compatibility with existing tests,
// use the long-hand query param name "sameness-group"
// rather than the alternative short-hand "sg"
r.params.Set("sameness-group", q.SamenessGroup)
}
if q.Datacenter != "" { if q.Datacenter != "" {
// For backwards-compatibility with existing tests, // For backwards-compatibility with existing tests,
// use the short-hand query param name "dc" // use the short-hand query param name "dc"

Loading…
Cancel
Save