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:
parent
8973b2f09f
commit
ee59a81dc9
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
|
@ -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`,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ deep-copy \
|
|||
-type DiscoveryRoute \
|
||||
-type DiscoverySplit \
|
||||
-type ExposeConfig \
|
||||
-type ExportedServicesConfigEntry \
|
||||
-type GatewayService \
|
||||
-type GatewayServiceTLSConfig \
|
||||
-type HTTPHeaderModifiers \
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue