Add sameness-group to exported-services config entries (#16836)

This PR adds the sameness-group field to exported-service
config entries, which allows for services to be exported
to multiple destination partitions / peers easily.
This commit is contained in:
Derek Menteer 2023-03-31 12:36:44 -05:00 committed by GitHub
parent bf64a33caa
commit 8d40cf9858
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 207 additions and 93 deletions

View File

@ -730,7 +730,9 @@ func validateProposedConfigEntryInServiceGraph(
entry := newEntry.(*structs.ExportedServicesConfigEntry)
_, serviceList, err := listServicesExportedToAnyPeerByConfigEntry(nil, tx, entry, nil)
_, serviceList, err := listServicesExportedToAnyPeerByConfigEntry(nil, tx, entry.EnterpriseMeta, map[configentry.KindName]structs.ConfigEntry{
configentry.NewKindNameForEntry(entry): entry,
})
if err != nil {
return err
}
@ -1486,7 +1488,7 @@ func readDiscoveryChainConfigEntriesTxn(
peerEntMeta := structs.DefaultEnterpriseMetaInPartition(entMeta.PartitionOrDefault())
for sg := range todoSamenessGroups {
idx, entry, err := getSamenessGroupConfigEntryTxn(tx, ws, sg, overrides, peerEntMeta)
idx, entry, err := getSamenessGroupConfigEntryTxn(tx, ws, sg, overrides, peerEntMeta.PartitionOrDefault())
if err != nil {
return 0, nil, err
}
@ -1720,32 +1722,6 @@ func getServiceIntentionsConfigEntryTxn(
return idx, ixn, nil
}
// getExportedServicesConfigEntryTxn is a convenience method for fetching a
// exported-services kind of config entry.
//
// If an override KEY is present for the requested config entry, the index
// returned will be 0. Any override VALUE (nil or otherwise) will be returned
// if there is a KEY match.
func getExportedServicesConfigEntryTxn(
tx ReadTxn,
ws memdb.WatchSet,
overrides map[configentry.KindName]structs.ConfigEntry,
entMeta *acl.EnterpriseMeta,
) (uint64, *structs.ExportedServicesConfigEntry, error) {
idx, entry, err := configEntryWithOverridesTxn(tx, ws, structs.ExportedServices, entMeta.PartitionOrDefault(), overrides, entMeta)
if err != nil {
return 0, nil, err
} else if entry == nil {
return idx, nil, nil
}
export, ok := entry.(*structs.ExportedServicesConfigEntry)
if !ok {
return 0, nil, fmt.Errorf("invalid service config type %T", entry)
}
return idx, export, nil
}
func configEntryWithOverridesTxn(
tx ReadTxn,
ws memdb.WatchSet,

View File

@ -0,0 +1,62 @@
package state
import (
"fmt"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/go-memdb"
)
// SimplifiedExportedServices contains a version of the exported-services that has
// been flattened by removing all of the sameness group references and replacing
// them with corresponding partition / peer entries.
type SimplifiedExportedServices structs.ExportedServicesConfigEntry
// ToPartitionMap is only used by the partition exporting logic.
// It returns a map[namespace][service] => []consuming_partitions
func (e *SimplifiedExportedServices) ToPartitionMap() map[string]map[string][]string {
resp := make(map[string]map[string][]string)
for _, svc := range e.Services {
if _, ok := resp[svc.Namespace]; !ok {
resp[svc.Namespace] = make(map[string][]string)
}
if _, ok := resp[svc.Namespace][svc.Name]; !ok {
consumers := make([]string, 0, len(svc.Consumers))
for _, c := range svc.Consumers {
if c.Partition != "" {
consumers = append(consumers, c.Partition)
}
}
resp[svc.Namespace][svc.Name] = consumers
}
}
return resp
}
// getExportedServicesConfigEntryTxn is a convenience method for fetching a
// exported-services kind of config entry.
//
// If an override KEY is present for the requested config entry, the index
// returned will be 0. Any override VALUE (nil or otherwise) will be returned
// if there is a KEY match.
func getExportedServicesConfigEntryTxn(
tx ReadTxn,
ws memdb.WatchSet,
overrides map[configentry.KindName]structs.ConfigEntry,
entMeta *acl.EnterpriseMeta,
) (uint64, *structs.ExportedServicesConfigEntry, error) {
idx, entry, err := configEntryWithOverridesTxn(tx, ws, structs.ExportedServices, entMeta.PartitionOrDefault(), overrides, entMeta)
if err != nil {
return 0, nil, err
} else if entry == nil {
return idx, nil, nil
}
export, ok := entry.(*structs.ExportedServicesConfigEntry)
if !ok {
return 0, nil, fmt.Errorf("invalid service config type %T", entry)
}
return idx, export, nil
}

View File

@ -0,0 +1,31 @@
//go:build !consulent
// +build !consulent
package state
import (
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/go-memdb"
)
func getSimplifiedExportedServices(
tx ReadTxn,
ws memdb.WatchSet,
overrides map[configentry.KindName]structs.ConfigEntry,
entMeta acl.EnterpriseMeta,
) (uint64, *SimplifiedExportedServices, error) {
idx, exports, err := getExportedServicesConfigEntryTxn(tx, ws, overrides, &entMeta)
if exports == nil {
return idx, nil, err
}
simple := SimplifiedExportedServices(*exports)
return idx, &simple, err
}
func (s *Store) GetSimplifiedExportedServices(ws memdb.WatchSet, entMeta acl.EnterpriseMeta) (uint64, *SimplifiedExportedServices, error) {
tx := s.db.Txn(false)
defer tx.Abort()
return getSimplifiedExportedServices(tx, ws, nil, entMeta)
}

View File

@ -9,7 +9,6 @@ package state
import (
"fmt"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/go-memdb"
@ -44,7 +43,7 @@ func getSamenessGroupConfigEntryTxn(
ws memdb.WatchSet,
name string,
overrides map[configentry.KindName]structs.ConfigEntry,
entMeta *acl.EnterpriseMeta,
partition string,
) (uint64, *structs.SamenessGroupConfigEntry, error) {
return 0, nil, nil
}

View File

@ -774,9 +774,9 @@ func exportedServicesForPeerTxn(
maxIdx := peering.ModifyIndex
entMeta := structs.NodeEnterpriseMetaInPartition(peering.Partition)
idx, exportConf, err := getExportedServicesConfigEntryTxn(tx, ws, nil, entMeta)
idx, exportConf, err := getSimplifiedExportedServices(tx, ws, nil, *entMeta)
if err != nil {
return 0, nil, fmt.Errorf("failed to fetch exported-services config entry: %w", err)
return 0, nil, fmt.Errorf("failed to fetch simplified exported-services config entry: %w", err)
}
if idx > maxIdx {
maxIdx = idx
@ -1019,17 +1019,8 @@ func listAllExportedServices(
overrides map[configentry.KindName]structs.ConfigEntry,
entMeta acl.EnterpriseMeta,
) (uint64, map[structs.ServiceName]struct{}, error) {
idx, export, err := getExportedServicesConfigEntryTxn(tx, ws, overrides, &entMeta)
if err != nil {
return 0, nil, err
}
found := make(map[structs.ServiceName]struct{})
if export == nil {
return idx, found, nil
}
_, services, err := listServicesExportedToAnyPeerByConfigEntry(ws, tx, export, overrides)
idx, services, err := listServicesExportedToAnyPeerByConfigEntry(ws, tx, entMeta, overrides)
if err != nil {
return 0, nil, err
}
@ -1044,16 +1035,25 @@ func listAllExportedServices(
func listServicesExportedToAnyPeerByConfigEntry(
ws memdb.WatchSet,
tx ReadTxn,
conf *structs.ExportedServicesConfigEntry,
entMeta acl.EnterpriseMeta,
overrides map[configentry.KindName]structs.ConfigEntry,
) (uint64, []structs.ServiceName, error) {
var (
entMeta = conf.GetEnterpriseMeta()
found = make(map[structs.ServiceName]struct{})
maxIdx uint64
)
idx, exports, err := getSimplifiedExportedServices(tx, ws, overrides, entMeta)
if err != nil {
return 0, nil, err
}
if idx > maxIdx {
maxIdx = idx
}
if exports == nil {
return 0, nil, nil
}
for _, svc := range conf.Services {
for _, svc := range exports.Services {
svcMeta := acl.NewEnterpriseMetaWithPartition(entMeta.PartitionOrDefault(), svc.Namespace)
sawPeer := false
@ -1412,17 +1412,12 @@ func peersForServiceTxn(
// Exported service config entries are scoped to partitions so they are in the default namespace.
partitionMeta := structs.DefaultEnterpriseMetaInPartition(entMeta.PartitionOrDefault())
idx, rawEntry, err := configEntryTxn(tx, ws, structs.ExportedServices, partitionMeta.PartitionOrDefault(), partitionMeta)
idx, exportedServices, err := getSimplifiedExportedServices(tx, ws, nil, *partitionMeta)
if err != nil {
return 0, nil, err
}
if rawEntry == nil {
return idx, nil, err
}
entry, ok := rawEntry.(*structs.ExportedServicesConfigEntry)
if !ok {
return 0, nil, fmt.Errorf("unexpected type %T for pbpeering.Peering index", rawEntry)
if exportedServices == nil {
return idx, nil, nil
}
var (
@ -1442,7 +1437,7 @@ func peersForServiceTxn(
// Namespace: *, Service: *
// Namespace: Exact, Service: *
// Namespace: Exact, Service: Exact
for i, service := range entry.Services {
for i, service := range exportedServices.Services {
switch {
case service.Namespace == structs.WildcardSpecifier:
wildcardNamespaceIdx = i
@ -1473,7 +1468,7 @@ func peersForServiceTxn(
return idx, results, nil
}
for _, c := range entry.Services[targetIdx].Consumers {
for _, c := range exportedServices.Services[targetIdx].Consumers {
if c.Peer != "" {
results = append(results, c.Peer)
}

View File

@ -41,43 +41,13 @@ type ExportedService struct {
// At most one of Partition or Peer must be specified.
type ServiceConsumer struct {
// Partition is the admin partition to export the service to.
// Deprecated: Peer should be used for both remote peers and local partitions.
Partition string `json:",omitempty"`
// Peer is the name of the peer to export the service to.
Peer string `json:",omitempty" alias:"peer_name"`
}
func (e *ExportedServicesConfigEntry) ToMap() map[string]map[string][]string {
resp := make(map[string]map[string][]string)
for _, svc := range e.Services {
if _, ok := resp[svc.Namespace]; !ok {
resp[svc.Namespace] = make(map[string][]string)
}
if _, ok := resp[svc.Namespace][svc.Name]; !ok {
consumers := make([]string, 0, len(svc.Consumers))
for _, c := range svc.Consumers {
consumers = append(consumers, c.Partition)
}
resp[svc.Namespace][svc.Name] = consumers
}
}
return resp
}
func (e *ExportedServicesConfigEntry) Clone() *ExportedServicesConfigEntry {
e2 := *e
e2.Services = make([]ExportedService, len(e.Services))
for _, svc := range e.Services {
exportedSvc := svc
exportedSvc.Consumers = make([]ServiceConsumer, len(svc.Consumers))
for _, consumer := range svc.Consumers {
exportedSvc.Consumers = append(exportedSvc.Consumers, consumer)
}
e2.Services = append(e2.Services, exportedSvc)
}
return &e2
// SamenessGroup is the name of the sameness group to export the service to.
SamenessGroup string `json:",omitempty" alias:"sameness_group"`
}
func (e *ExportedServicesConfigEntry) GetKind() string {
@ -122,6 +92,14 @@ func (e *ExportedServicesConfigEntry) Validate() error {
return err
}
if err := e.validateServicesEnterprise(); err != nil {
return err
}
return e.validateServices()
}
func (e *ExportedServicesConfigEntry) validateServices() error {
for i, svc := range e.Services {
if svc.Name == "" {
return fmt.Errorf("Services[%d]: service name cannot be empty", i)
@ -133,8 +111,18 @@ func (e *ExportedServicesConfigEntry) Validate() error {
return fmt.Errorf("Services[%d]: must have at least one consumer", i)
}
for j, consumer := range svc.Consumers {
if consumer.Peer != "" && consumer.Partition != "" {
return fmt.Errorf("Services[%d].Consumers[%d]: must define at most one of Peer or Partition", i, j)
count := 0
if consumer.Peer != "" {
count++
}
if consumer.Partition != "" {
count++
}
if consumer.SamenessGroup != "" {
count++
}
if count > 1 {
return fmt.Errorf("Services[%d].Consumers[%d]: must define at most one of Peer, Partition, or SamenessGroup", i, j)
}
if consumer.Partition == WildcardSpecifier {
return fmt.Errorf("Services[%d].Consumers[%d]: exporting to all partitions (wildcard) is not supported", i, j)

View File

@ -0,0 +1,24 @@
//go:build !consulent
// +build !consulent
package structs
import (
"fmt"
"github.com/hashicorp/consul/acl"
)
func (e *ExportedServicesConfigEntry) validateServicesEnterprise() error {
for i, svc := range e.Services {
for j, consumer := range svc.Consumers {
if !acl.IsDefaultPartition(consumer.Partition) {
return fmt.Errorf("Services[%d].Consumers[%d]: partitions are an enterprise-only feature", i, j)
}
if consumer.SamenessGroup != "" {
return fmt.Errorf("Services[%d].Consumers[%d]: sameness-groups are an enterprise-only feature", i, j)
}
}
}
return nil
}

View File

@ -59,6 +59,22 @@ func TestExportedServicesConfigEntry_OSS(t *testing.T) {
},
validateErr: `exported-services Name must be "default"`,
},
"validate: sameness groups are enterprise only": {
entry: &ExportedServicesConfigEntry{
Name: "default",
Services: []ExportedService{
{
Name: "web",
Consumers: []ServiceConsumer{
{
SamenessGroup: "sg",
},
},
},
},
},
validateErr: `Services[0].Consumers[0]: sameness-groups are an enterprise-only feature`,
},
}
testConfigEntryNormalizeAndValidate(t, cases)

View File

@ -89,7 +89,7 @@ func TestExportedServicesConfigEntry(t *testing.T) {
},
},
},
validateErr: `Services[0].Consumers[0]: must define at most one of Peer or Partition`,
validateErr: `Services[0].Consumers[0]: must define at most one of Peer, Partition, or SamenessGroup`,
},
}

View File

@ -23,6 +23,7 @@ deep-copy \
-type DiscoveryRoute \
-type DiscoverySplit \
-type ExposeConfig \
-type ExportedServicesConfigEntry \
-type GatewayService \
-type GatewayServiceTLSConfig \
-type HTTPHeaderModifiers \

View File

@ -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 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 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.
package structs
@ -270,6 +270,28 @@ func (o *ExposeConfig) DeepCopy() *ExposeConfig {
return &cp
}
// DeepCopy generates a deep copy of *ExportedServicesConfigEntry
func (o *ExportedServicesConfigEntry) DeepCopy() *ExportedServicesConfigEntry {
var cp ExportedServicesConfigEntry = *o
if o.Services != nil {
cp.Services = make([]ExportedService, len(o.Services))
copy(cp.Services, o.Services)
for i2 := range o.Services {
if o.Services[i2].Consumers != nil {
cp.Services[i2].Consumers = make([]ServiceConsumer, len(o.Services[i2].Consumers))
copy(cp.Services[i2].Consumers, o.Services[i2].Consumers)
}
}
}
if o.Meta != nil {
cp.Meta = make(map[string]string, len(o.Meta))
for k2, v2 := range o.Meta {
cp.Meta[k2] = v2
}
}
return &cp
}
// DeepCopy generates a deep copy of *GatewayService
func (o *GatewayService) DeepCopy() *GatewayService {
var cp GatewayService = *o