From ee59a81dc9c8ca5b782861228e6135f6049c6abf Mon Sep 17 00:00:00 2001 From: Derek Menteer <105233703+hashi-derek@users.noreply.github.com> Date: Fri, 31 Mar 2023 12:36:44 -0500 Subject: [PATCH] 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. --- agent/consul/state/config_entry.go | 32 ++-------- .../state/config_entry_exported_services.go | 62 +++++++++++++++++++ .../config_entry_exported_services_oss.go | 31 ++++++++++ .../state/config_entry_sameness_group_oss.go | 3 +- agent/consul/state/peering.go | 49 +++++++-------- agent/structs/config_entry_exports.go | 56 +++++++---------- agent/structs/config_entry_exports_oss.go | 24 +++++++ ...st.go => config_entry_exports_oss_test.go} | 16 +++++ agent/structs/config_entry_exports_test.go | 2 +- agent/structs/deep-copy.sh | 1 + agent/structs/structs.deepcopy.go | 24 ++++++- 11 files changed, 207 insertions(+), 93 deletions(-) create mode 100644 agent/consul/state/config_entry_exported_services.go create mode 100644 agent/consul/state/config_entry_exported_services_oss.go create mode 100644 agent/structs/config_entry_exports_oss.go rename agent/structs/{config_entry_export_oss_test.go => config_entry_exports_oss_test.go} (77%) diff --git a/agent/consul/state/config_entry.go b/agent/consul/state/config_entry.go index 1bc35a0fc..44626f6c1 100644 --- a/agent/consul/state/config_entry.go +++ b/agent/consul/state/config_entry.go @@ -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, diff --git a/agent/consul/state/config_entry_exported_services.go b/agent/consul/state/config_entry_exported_services.go new file mode 100644 index 000000000..a0ecd3572 --- /dev/null +++ b/agent/consul/state/config_entry_exported_services.go @@ -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 +} diff --git a/agent/consul/state/config_entry_exported_services_oss.go b/agent/consul/state/config_entry_exported_services_oss.go new file mode 100644 index 000000000..968137477 --- /dev/null +++ b/agent/consul/state/config_entry_exported_services_oss.go @@ -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) +} diff --git a/agent/consul/state/config_entry_sameness_group_oss.go b/agent/consul/state/config_entry_sameness_group_oss.go index b5110ac84..14765363c 100644 --- a/agent/consul/state/config_entry_sameness_group_oss.go +++ b/agent/consul/state/config_entry_sameness_group_oss.go @@ -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 } diff --git a/agent/consul/state/peering.go b/agent/consul/state/peering.go index ba28441f4..b8e2fd10c 100644 --- a/agent/consul/state/peering.go +++ b/agent/consul/state/peering.go @@ -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 + 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) } diff --git a/agent/structs/config_entry_exports.go b/agent/structs/config_entry_exports.go index 6795af316..631773072 100644 --- a/agent/structs/config_entry_exports.go +++ b/agent/structs/config_entry_exports.go @@ -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) diff --git a/agent/structs/config_entry_exports_oss.go b/agent/structs/config_entry_exports_oss.go new file mode 100644 index 000000000..bd766567d --- /dev/null +++ b/agent/structs/config_entry_exports_oss.go @@ -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 +} diff --git a/agent/structs/config_entry_export_oss_test.go b/agent/structs/config_entry_exports_oss_test.go similarity index 77% rename from agent/structs/config_entry_export_oss_test.go rename to agent/structs/config_entry_exports_oss_test.go index 72be5aad6..0c3982f73 100644 --- a/agent/structs/config_entry_export_oss_test.go +++ b/agent/structs/config_entry_exports_oss_test.go @@ -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) diff --git a/agent/structs/config_entry_exports_test.go b/agent/structs/config_entry_exports_test.go index dd92a1139..7905b4600 100644 --- a/agent/structs/config_entry_exports_test.go +++ b/agent/structs/config_entry_exports_test.go @@ -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`, }, } diff --git a/agent/structs/deep-copy.sh b/agent/structs/deep-copy.sh index 75856d364..e4ab69273 100755 --- a/agent/structs/deep-copy.sh +++ b/agent/structs/deep-copy.sh @@ -23,6 +23,7 @@ deep-copy \ -type DiscoveryRoute \ -type DiscoverySplit \ -type ExposeConfig \ + -type ExportedServicesConfigEntry \ -type GatewayService \ -type GatewayServiceTLSConfig \ -type HTTPHeaderModifiers \ diff --git a/agent/structs/structs.deepcopy.go b/agent/structs/structs.deepcopy.go index c678914b7..95e50b453 100644 --- a/agent/structs/structs.deepcopy.go +++ b/agent/structs/structs.deepcopy.go @@ -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