Add default resolvers to disco chains based on the default sameness group (#16837)

This commit is contained in:
Eric Haberkorn 2023-03-31 14:35:56 -04:00 committed by GitHub
parent ee59a81dc9
commit beb9f17337
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 219 additions and 88 deletions

View File

@ -13,13 +13,14 @@ import (
//
// None of these are defaulted.
type DiscoveryChainSet struct {
Routers map[structs.ServiceID]*structs.ServiceRouterConfigEntry
Splitters map[structs.ServiceID]*structs.ServiceSplitterConfigEntry
Resolvers map[structs.ServiceID]*structs.ServiceResolverConfigEntry
Services map[structs.ServiceID]*structs.ServiceConfigEntry
Peers map[string]*pbpeering.Peering
SamenessGroups map[string]*structs.SamenessGroupConfigEntry
ProxyDefaults map[string]*structs.ProxyConfigEntry
Routers map[structs.ServiceID]*structs.ServiceRouterConfigEntry
Splitters map[structs.ServiceID]*structs.ServiceSplitterConfigEntry
Resolvers map[structs.ServiceID]*structs.ServiceResolverConfigEntry
Services map[structs.ServiceID]*structs.ServiceConfigEntry
Peers map[string]*pbpeering.Peering
DefaultSamenessGroup *structs.SamenessGroupConfigEntry
SamenessGroups map[string]*structs.SamenessGroupConfigEntry
ProxyDefaults map[string]*structs.ProxyConfigEntry
}
func NewDiscoveryChainSet() *DiscoveryChainSet {
@ -69,6 +70,10 @@ func (e *DiscoveryChainSet) GetSamenessGroup(name string) *structs.SamenessGroup
return nil
}
func (e *DiscoveryChainSet) GetDefaultSamenessGroup() *structs.SamenessGroupConfigEntry {
return e.DefaultSamenessGroup
}
func (e *DiscoveryChainSet) GetProxyDefaults(partition string) *structs.ProxyConfigEntry {
if e.ProxyDefaults != nil {
return e.ProxyDefaults[partition]
@ -116,9 +121,9 @@ func (e *DiscoveryChainSet) AddServices(entries ...*structs.ServiceConfigEntry)
}
}
// AddSamenessGroup adds service configs. Convenience function for testing.
// AddSamenessGroup adds a sameness group. Convenience function for testing.
func (e *DiscoveryChainSet) AddSamenessGroup(entries ...*structs.SamenessGroupConfigEntry) {
if e.Services == nil {
if e.SamenessGroups == nil {
e.SamenessGroups = make(map[string]*structs.SamenessGroupConfigEntry)
}
for _, entry := range entries {
@ -126,6 +131,20 @@ func (e *DiscoveryChainSet) AddSamenessGroup(entries ...*structs.SamenessGroupCo
}
}
// SetDefaultSamenessGroup sets the default sameness group. Convenience function for testing.
func (e *DiscoveryChainSet) SetDefaultSamenessGroup(entry *structs.SamenessGroupConfigEntry) {
if e.SamenessGroups == nil {
e.SamenessGroups = make(map[string]*structs.SamenessGroupConfigEntry)
}
if entry == nil {
return
}
e.SamenessGroups[entry.Name] = entry
e.DefaultSamenessGroup = entry
}
// AddProxyDefaults adds proxy-defaults configs. Convenience function for testing.
func (e *DiscoveryChainSet) AddProxyDefaults(entries ...*structs.ProxyConfigEntry) {
if e.ProxyDefaults == nil {
@ -149,23 +168,26 @@ func (e *DiscoveryChainSet) AddPeers(entries ...*pbpeering.Peering) {
// AddEntries adds generic configs. Convenience function for testing. Panics on
// operator error.
func (e *DiscoveryChainSet) AddEntries(entries ...structs.ConfigEntry) {
for _, entry := range entries {
switch entry.GetKind() {
case structs.ServiceRouter:
e.AddRouters(entry.(*structs.ServiceRouterConfigEntry))
case structs.ServiceSplitter:
e.AddSplitters(entry.(*structs.ServiceSplitterConfigEntry))
case structs.ServiceResolver:
e.AddResolvers(entry.(*structs.ServiceResolverConfigEntry))
case structs.ServiceDefaults:
e.AddServices(entry.(*structs.ServiceConfigEntry))
case structs.SamenessGroup:
e.AddSamenessGroup(entry.(*structs.SamenessGroupConfigEntry))
case structs.ProxyDefaults:
for _, rawEntry := range entries {
switch entry := rawEntry.(type) {
case *structs.ServiceRouterConfigEntry:
e.AddRouters(entry)
case *structs.ServiceSplitterConfigEntry:
e.AddSplitters(entry)
case *structs.ServiceResolverConfigEntry:
e.AddResolvers(entry)
case *structs.ServiceConfigEntry:
e.AddServices(entry)
case *structs.SamenessGroupConfigEntry:
if entry.DefaultForFailover {
e.DefaultSamenessGroup = entry
}
e.AddSamenessGroup(entry)
case *structs.ProxyConfigEntry:
if entry.GetName() != structs.ProxyConfigGlobal {
panic("the only supported proxy-defaults name is '" + structs.ProxyConfigGlobal + "'")
}
e.AddProxyDefaults(entry.(*structs.ProxyConfigEntry))
e.AddProxyDefaults(entry)
default:
panic("unhandled config entry kind: " + entry.GetKind())
}
@ -182,5 +204,5 @@ func (e *DiscoveryChainSet) IsEmpty() bool {
// service-splitters, or service-resolvers that are present. These config
// entries are the primary parts of the discovery chain.
func (e *DiscoveryChainSet) IsChainEmpty() bool {
return len(e.Routers) == 0 && len(e.Splitters) == 0 && len(e.Resolvers) == 0
return len(e.Routers) == 0 && len(e.Splitters) == 0 && len(e.Resolvers) == 0 && e.DefaultSamenessGroup == nil
}

View File

@ -582,7 +582,7 @@ func (c *compiler) assembleChain() error {
// Check for short circuit path.
if len(c.resolvers) == 0 && c.entries.IsChainEmpty() {
// Materialize defaults and cache.
c.resolvers[sid] = newDefaultServiceResolver(sid)
c.resolvers[sid] = c.newDefaultServiceResolver(sid, "")
}
// The only router we consult is the one for the service name at the top of
@ -923,7 +923,7 @@ RESOLVE_AGAIN:
resolver, ok := c.resolvers[targetID]
if !ok {
// Materialize defaults and cache.
resolver = newDefaultServiceResolver(targetID)
resolver = c.newDefaultServiceResolver(targetID, target.Peer)
c.resolvers[targetID] = resolver
}
@ -1095,7 +1095,8 @@ RESOLVE_AGAIN:
}
if resolver.Redirect != nil && resolver.Redirect.SamenessGroup != "" {
opts := resolver.Redirect.ToDiscoveryTargetOpts()
opts := structs.MergeDiscoveryTargetOpts(resolver.ToSamenessDiscoveryTargetOpts(),
resolver.Redirect.ToDiscoveryTargetOpts())
failoverTargets, err = c.makeSamenessGroupFailover(target, opts, resolver.Redirect.SamenessGroup)
if err != nil {
return nil, err
@ -1138,7 +1139,9 @@ RESOLVE_AGAIN:
}
}
} else if failover.SamenessGroup != "" {
failoverTargets, err = c.makeSamenessGroupFailover(target, failover.ToDiscoveryTargetOpts(), failover.SamenessGroup)
opts := structs.MergeDiscoveryTargetOpts(resolver.ToSamenessDiscoveryTargetOpts(),
failover.ToDiscoveryTargetOpts())
failoverTargets, err = c.makeSamenessGroupFailover(target, opts, failover.SamenessGroup)
if err != nil {
return nil, err
}
@ -1199,7 +1202,23 @@ func (c *compiler) makeSamenessGroupFailover(target *structs.DiscoveryTarget, op
return failoverTargets, nil
}
func newDefaultServiceResolver(sid structs.ServiceID) *structs.ServiceResolverConfigEntry {
func (c *compiler) newDefaultServiceResolver(sid structs.ServiceID, peer string) *structs.ServiceResolverConfigEntry {
sg := c.entries.GetDefaultSamenessGroup()
entMeta := c.GetEnterpriseMeta()
if sg != nil && peer == "" && (entMeta == nil || sid.PartitionOrDefault() == entMeta.PartitionOrDefault()) {
return &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: sid.ID,
EnterpriseMeta: sid.EnterpriseMeta,
// This needs to be a redirect rather than failover because failovers
// implicitly include the local service. This isn't the behavior we want
// for services on sameness groups the local partition isn't a member of.
Redirect: &structs.ServiceResolverRedirect{
SamenessGroup: sg.Name,
},
}
}
return &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver,
Name: sid.ID,

View File

@ -751,14 +751,70 @@ func validateProposedConfigEntryInServiceGraph(
case structs.SamenessGroup:
// Any service resolver could reference a sameness group.
_, entries, err := configEntriesByKindTxn(tx, nil, structs.ServiceResolver, wildcardEntMeta)
_, resolverEntries, err := configEntriesByKindTxn(tx, nil, structs.ServiceResolver, wildcardEntMeta)
if err != nil {
return err
}
for _, entry := range entries {
for _, entry := range resolverEntries {
checkChains[structs.NewServiceID(entry.GetName(), entry.GetEnterpriseMeta())] = struct{}{}
}
// This is the case for deleting a config entry
if newEntry == nil {
break
}
entry := newEntry.(*structs.SamenessGroupConfigEntry)
_, samenessGroupEntries, err := configEntriesByKindTxn(tx, nil, structs.SamenessGroup, wildcardEntMeta)
if err != nil {
return err
}
// Replace the existing sameness group if one exists.
var exists bool
for i := range samenessGroupEntries {
sg := samenessGroupEntries[i]
if sg.GetName() == entry.Name {
samenessGroupEntries[i] = entry
exists = true
break
}
}
// If this sameness group doesn't currently exist, add it.
if !exists {
samenessGroupEntries = append(samenessGroupEntries, entry)
}
existingPartitions := make(map[string]string)
existingPeers := make(map[string]string)
for _, e := range samenessGroupEntries {
sg, ok := e.(*structs.SamenessGroupConfigEntry)
if !ok {
return fmt.Errorf("type %T is not a sameness group config entry", e)
}
for _, m := range sg.AllMembers() {
if m.Peer != "" {
if prev, ok := existingPeers[m.Peer]; ok {
return fmt.Errorf("members can only belong to a single sameness group, but cluster peer %q is shared between groups %q and %q",
m.Peer, prev, sg.Name,
)
}
existingPeers[m.Peer] = sg.Name
continue
}
if prev, ok := existingPartitions[m.Partition]; ok {
return fmt.Errorf("members can only belong to a single sameness group, but partition %q is shared between groups %q and %q",
m.Partition, prev, sg.Name,
)
}
existingPartitions[m.Partition] = sg.Name
}
}
case structs.ProxyDefaults:
// Check anything that has a discovery chain entry. In the future we could
// somehow omit the ones that have a default protocol configured.
@ -1499,14 +1555,29 @@ func readDiscoveryChainConfigEntriesTxn(
continue
}
for _, e := range entry.Members {
if e.Peer != "" {
todoPeers[e.Peer] = struct{}{}
}
for _, peer := range entry.RelatedPeers() {
todoPeers[peer] = struct{}{}
}
res.SamenessGroups[sg] = entry
}
if r := res.Resolvers[sid]; r == nil {
idx, sg, err := getDefaultSamenessGroup(tx, ws, sid.PartitionOrDefault())
if err != nil {
return 0, nil, err
}
if idx > maxIdx {
maxIdx = idx
}
if sg != nil {
res.DefaultSamenessGroup = sg
res.SamenessGroups[sg.Name] = sg
for _, peer := range sg.RelatedPeers() {
todoPeers[peer] = struct{}{}
}
}
}
for peerName := range todoPeers {
q := Query{
Value: peerName,

View File

@ -14,7 +14,7 @@ import (
"github.com/hashicorp/go-memdb"
)
// SamnessGroupDefaultIndex is a placeholder for OSS. Sameness-groups are enterprise only.
// SamenessGroupDefaultIndex is a placeholder for OSS. Sameness-groups are enterprise only.
type SamenessGroupDefaultIndex struct{}
var _ memdb.Indexer = (*SamenessGroupDefaultIndex)(nil)
@ -47,3 +47,7 @@ func getSamenessGroupConfigEntryTxn(
) (uint64, *structs.SamenessGroupConfigEntry, error) {
return 0, nil, nil
}
func getDefaultSamenessGroup(tx ReadTxn, ws memdb.WatchSet, partition string) (uint64, *structs.SamenessGroupConfigEntry, error) {
return 0, nil, nil
}

View File

@ -17,7 +17,7 @@ const (
indexLink = "link"
indexIntentionLegacyID = "intention-legacy-id"
indexSource = "intention-source"
indexSamenessGroupDefault = "sameness-group-default"
indexSamenessGroupDefault = "sameness-group-default-for-failover"
)
// configTableSchema returns a new table schema used to store global

View File

@ -61,7 +61,8 @@ func setupTestVariationConfigEntriesAndSnapshot(
}
dbChainID := structs.ChainID(dbOpts)
makeChainID := func(opts structs.DiscoveryTargetOpts) string {
return structs.ChainID(structs.MergeDiscoveryTargetOpts(dbOpts, opts))
finalOpts := structs.MergeDiscoveryTargetOpts(dbOpts, opts)
return structs.ChainID(finalOpts)
}
switch variation {

View File

@ -1367,6 +1367,16 @@ type ServiceResolverRedirect struct {
SamenessGroup string `json:",omitempty"`
}
// ToSamenessDiscoveryTargetOpts returns the options required for sameness failover and redirects.
// These operations should preserve the service name and namespace.
func (r *ServiceResolverConfigEntry) ToSamenessDiscoveryTargetOpts() DiscoveryTargetOpts {
return DiscoveryTargetOpts{
Service: r.Name,
Namespace: r.NamespaceOrDefault(),
Partition: r.PartitionOrDefault(),
}
}
func (r *ServiceResolverRedirect) ToDiscoveryTargetOpts() DiscoveryTargetOpts {
return DiscoveryTargetOpts{
Service: r.Service,

View File

@ -12,7 +12,8 @@ import (
type SamenessGroupConfigEntry struct {
Name string
IsDefault bool `json:",omitempty" alias:"is_default"`
DefaultForFailover bool `json:",omitempty" alias:"default_for_failover"`
IncludeLocal bool `json:",omitempty" alias:"include_local"`
Members []SamenessGroupMember
Meta map[string]string `json:",omitempty"`
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
@ -73,20 +74,3 @@ type SamenessGroupMember struct {
Partition string
Peer string
}
func (s *SamenessGroupConfigEntry) ToFailoverTargets() []ServiceResolverFailoverTarget {
if s == nil {
return nil
}
var targets []ServiceResolverFailoverTarget
for _, m := range s.Members {
targets = append(targets, ServiceResolverFailoverTarget{
Peer: m.Peer,
Partition: m.Partition,
})
}
return targets
}

View File

@ -11,3 +11,17 @@ import "fmt"
func (s *SamenessGroupConfigEntry) Validate() error {
return fmt.Errorf("sameness-groups are an enterprise-only feature")
}
// RelatedPeers returns all peers that are members of a sameness group config entry.
func (s *SamenessGroupConfigEntry) RelatedPeers() []string {
return nil
}
// AllMembers adds the local partition to Members when it is set.
func (s *SamenessGroupConfigEntry) AllMembers() []SamenessGroupMember {
return nil
}
func (s *SamenessGroupConfigEntry) ToFailoverTargets() []ServiceResolverFailoverTarget {
return nil
}

View File

@ -266,32 +266,37 @@ type DiscoveryTargetOpts struct {
Peer string
}
func MergeDiscoveryTargetOpts(o1 DiscoveryTargetOpts, o2 DiscoveryTargetOpts) DiscoveryTargetOpts {
if o2.Service != "" {
o1.Service = o2.Service
func MergeDiscoveryTargetOpts(opts ...DiscoveryTargetOpts) DiscoveryTargetOpts {
var final DiscoveryTargetOpts
for _, o := range opts {
if o.Service != "" {
final.Service = o.Service
}
if o.ServiceSubset != "" {
final.ServiceSubset = o.ServiceSubset
}
// default should override the existing value
if o.Namespace != "" {
final.Namespace = o.Namespace
}
// default should override the existing value
if o.Partition != "" {
final.Partition = o.Partition
}
if o.Datacenter != "" {
final.Datacenter = o.Datacenter
}
if o.Peer != "" {
final.Peer = o.Peer
}
}
if o2.ServiceSubset != "" {
o1.ServiceSubset = o2.ServiceSubset
}
if o2.Namespace != "" {
o1.Namespace = o2.Namespace
}
if o2.Partition != "" {
o1.Partition = o2.Partition
}
if o2.Datacenter != "" {
o1.Datacenter = o2.Datacenter
}
if o2.Peer != "" {
o1.Peer = o2.Peer
}
return o1
return final
}
func NewDiscoveryTarget(opts DiscoveryTargetOpts) *DiscoveryTarget {

View File

@ -4,14 +4,15 @@
package api
type SamenessGroupConfigEntry struct {
Kind string
Name string
Partition string `json:",omitempty"`
IsDefault bool `json:",omitempty" alias:"is_default"`
Members []SamenessGroupMember
Meta map[string]string `json:",omitempty"`
CreateIndex uint64
ModifyIndex uint64
Kind string
Name string
Partition string `json:",omitempty"`
DefaultForFailover bool `json:",omitempty" alias:"default_for_failover"`
IncludeLocal bool `json:",omitempty" alias:"include_local"`
Members []SamenessGroupMember
Meta map[string]string `json:",omitempty"`
CreateIndex uint64
ModifyIndex uint64
}
type SamenessGroupMember struct {