open-consul/agent/consul/state/config_entry_intention.go
2023-05-25 12:18:55 -04:00

509 lines
14 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package state
import (
"fmt"
"sort"
memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
)
type ServiceIntentionLegacyIDIndex struct {
uuidFieldIndex memdb.UUIDFieldIndex // for helper code
}
func (s *ServiceIntentionLegacyIDIndex) FromObject(obj interface{}) (bool, [][]byte, error) {
entry, ok := obj.(structs.ConfigEntry)
if !ok {
return false, nil, fmt.Errorf("object is not a ConfigEntry")
}
if entry.GetKind() != structs.ServiceIntentions {
return false, nil, nil
}
ixnEntry, ok := entry.(*structs.ServiceIntentionsConfigEntry)
if !ok {
return false, nil, nil
}
// We don't pre-size this slice because it will only be populated
// for legacy data, which should reduce over time.
var vals [][]byte
for _, src := range ixnEntry.Sources {
if src.LegacyID != "" {
arg, err := s.FromArgs(src.LegacyID)
if err != nil {
return false, nil, err
}
vals = append(vals, arg)
}
}
if len(vals) == 0 {
return false, nil, nil
}
return true, vals, nil
}
func (s *ServiceIntentionLegacyIDIndex) FromArgs(args ...interface{}) ([]byte, error) {
arg, err := s.uuidFieldIndex.FromArgs(args...)
if err != nil {
return nil, err
}
// Add the null character as a terminator
b := make([]byte, 0, len(arg)+1)
b = append(b, arg...)
b = append(b, '\x00')
return b, nil
}
func (s *ServiceIntentionLegacyIDIndex) PrefixFromArgs(args ...interface{}) ([]byte, error) {
val, err := s.FromArgs(args...)
if err != nil {
return nil, err
}
// Strip the null terminator, the rest is a prefix
n := len(val)
if n > 0 {
return val[:n-1], nil
}
return val, nil
}
type SamenessGroupMemberIndex struct {
}
// Compile-time assert that these interfaces hold to ensure that the
// methods correctly exist across the oss/ent split.
var _ memdb.Indexer = (*SamenessGroupMemberIndex)(nil)
var _ memdb.MultiIndexer = (*SamenessGroupMemberIndex)(nil)
func (s *SamenessGroupMemberIndex) FromObject(obj interface{}) (bool, [][]byte, error) {
entry, ok := obj.(structs.ConfigEntry)
if !ok {
return false, nil, fmt.Errorf("object is not a ConfigEntry")
}
sg, ok := entry.(*structs.SamenessGroupConfigEntry)
if !ok {
return false, nil, nil
}
vals := make([][]byte, 0)
for _, m := range sg.AllMembers() {
if m.Partition == "" {
continue
}
// add 1 for null separator after each string
buf := newIndexBuilder(len(m.Partition) + 1)
buf.String(m.Partition)
vals = append(vals, buf.Bytes())
}
if len(vals) == 0 {
return false, nil, nil
}
return true, vals, nil
}
func (s *SamenessGroupMemberIndex) FromArgs(args ...interface{}) ([]byte, error) {
if len(args) != 1 {
return nil, fmt.Errorf("must provide only a single argument")
}
arg, ok := args[0].(string)
if !ok {
return nil, fmt.Errorf("argument must be a string: %#v", args[0])
}
buf := newIndexBuilder(len(arg) + 1)
buf.String(arg)
// Add the null character as a terminator
return buf.Bytes(), nil
}
type ServiceIntentionSourceSamenessGroupIndex struct {
}
// Compile-time assert that these interfaces hold to ensure that the
// methods correctly exist across the oss/ent split.
var _ memdb.Indexer = (*ServiceIntentionSourceSamenessGroupIndex)(nil)
var _ memdb.MultiIndexer = (*ServiceIntentionSourceSamenessGroupIndex)(nil)
func (s *ServiceIntentionSourceSamenessGroupIndex) FromObject(obj interface{}) (bool, [][]byte, error) {
entry, ok := obj.(structs.ConfigEntry)
if !ok {
return false, nil, fmt.Errorf("object is not a ConfigEntry")
}
ixnEntry, ok := entry.(*structs.ServiceIntentionsConfigEntry)
if !ok {
return false, nil, nil
}
vals := make([][]byte, 0, len(ixnEntry.Sources))
for _, src := range ixnEntry.Sources {
sg := src.SamenessGroup
if sg == "" {
continue
}
sn := structs.ServiceName{
Name: src.Name,
EnterpriseMeta: acl.NewEnterpriseMetaWithPartition(ixnEntry.PartitionOrDefault(), src.NamespaceOrDefault()),
}.String()
// add 2 for null separator after each string
buf := newIndexBuilder(len(sg) + len(sn) + 2)
buf.String(sg)
buf.String(sn)
vals = append(vals, buf.Bytes())
}
if len(vals) == 0 {
return false, nil, nil
}
return true, vals, nil
}
func (s *ServiceIntentionSourceSamenessGroupIndex) FromArgs(args ...interface{}) ([]byte, error) {
if len(args) != 1 {
return nil, fmt.Errorf("must provide only a single argument")
}
arg, ok := args[0].(structs.ServiceNameWithSamenessGroup)
if !ok {
return nil, fmt.Errorf("argument must be a structs.ServiceID: %#v", args[0])
}
// Intention queries cannot use a peered service as a source
sg := arg.SamenessGroup
sn := arg.ServiceName.String()
// add 2 for null separator after each string
buf := newIndexBuilder(len(sg) + len(sn) + 2)
buf.String(sg)
buf.String(sn)
// Add the null character as a terminator
return buf.Bytes(), nil
}
type ServiceIntentionSourceIndex struct {
}
// Compile-time assert that these interfaces hold to ensure that the
// methods correctly exist across the oss/ent split.
var _ memdb.Indexer = (*ServiceIntentionSourceIndex)(nil)
var _ memdb.MultiIndexer = (*ServiceIntentionSourceIndex)(nil)
func (s *ServiceIntentionSourceIndex) FromObject(obj interface{}) (bool, [][]byte, error) {
entry, ok := obj.(structs.ConfigEntry)
if !ok {
return false, nil, fmt.Errorf("object is not a ConfigEntry")
}
if entry.GetKind() != structs.ServiceIntentions {
return false, nil, nil
}
ixnEntry, ok := entry.(*structs.ServiceIntentionsConfigEntry)
if !ok {
return false, nil, nil
}
vals := make([][]byte, 0, len(ixnEntry.Sources))
for _, src := range ixnEntry.Sources {
if src.SamenessGroup != "" {
continue
}
peer := src.Peer
if peer == "" {
peer = structs.LocalPeerKeyword
}
sn := src.SourceServiceName().String()
// add 2 for null separator after each string
buf := newIndexBuilder(len(peer) + len(sn) + 2)
buf.String(peer)
buf.String(sn)
vals = append(vals, buf.Bytes())
}
if len(vals) == 0 {
return false, nil, nil
}
return true, vals, nil
}
func (s *ServiceIntentionSourceIndex) FromArgs(args ...interface{}) ([]byte, error) {
if len(args) != 1 {
return nil, fmt.Errorf("must provide only a single argument")
}
arg, ok := args[0].(structs.ServiceName)
if !ok {
return nil, fmt.Errorf("argument must be a structs.ServiceID: %#v", args[0])
}
// Intention queries cannot use a peered service as a source
peer := structs.LocalPeerKeyword
sn := arg.String()
// add 2 for null separator after each string
buf := newIndexBuilder(len(peer) + len(sn) + 2)
buf.String(peer)
buf.String(sn)
// Add the null character as a terminator
return buf.Bytes(), nil
}
func configIntentionsListTxn(tx ReadTxn, ws memdb.WatchSet, entMeta *acl.EnterpriseMeta) (uint64, structs.Intentions, bool, error) {
// unrolled part of configEntriesByKindTxn
idx := maxIndexTxn(tx, tableConfigEntries)
iter, err := getAllConfigEntriesByKindWithTxn(tx, structs.ServiceIntentions)
if err != nil {
return 0, nil, false, fmt.Errorf("failed config entry lookup: %s", err)
}
ws.Add(iter.WatchCh())
results := configIntentionsConvertToList(iter, entMeta)
// Sort by precedence just because that's nicer and probably what most clients
// want for presentation.
sort.Sort(structs.IntentionPrecedenceSorter(results))
return idx, results, true, nil
}
func configIntentionGetTxn(tx ReadTxn, ws memdb.WatchSet, id string) (uint64, *structs.ServiceIntentionsConfigEntry, *structs.Intention, error) {
idx := maxIndexTxn(tx, tableConfigEntries)
if idx < 1 {
idx = 1
}
watchCh, existing, err := tx.FirstWatch(tableConfigEntries, indexIntentionLegacyID, id)
if err != nil {
return 0, nil, nil, fmt.Errorf("failed config entry lookup: %s", err)
}
ws.Add(watchCh)
if existing == nil {
return idx, nil, nil, nil
}
conf, ok := existing.(*structs.ServiceIntentionsConfigEntry)
if !ok {
return 0, nil, nil, fmt.Errorf("config entry is an invalid type: %T", conf)
}
for _, src := range conf.Sources {
if src.LegacyID == id {
return idx, conf, conf.ToIntention(src), nil
}
}
return idx, nil, nil, nil // Shouldn't happen.
}
func (s *Store) configIntentionGetExactTxn(tx ReadTxn, ws memdb.WatchSet, args *structs.IntentionQueryExact) (uint64, *structs.ServiceIntentionsConfigEntry, *structs.Intention, error) {
if err := args.Validate(); err != nil {
return 0, nil, nil, err
}
idx, entry, err := getServiceIntentionsConfigEntryTxn(tx, ws, args.DestinationName, nil, args.DestinationEnterpriseMeta())
if err != nil {
return 0, nil, nil, err
} else if entry == nil {
return idx, nil, nil, nil
}
psn := structs.PeeredServiceName{
Peer: args.SourcePeer,
ServiceName: structs.NewServiceName(args.SourceName, args.SourceEnterpriseMeta()),
}
for _, src := range entry.Sources {
if psn.Peer == src.Peer && psn.ServiceName == src.SourceServiceName() {
return idx, entry, entry.ToIntention(src), nil
}
}
return idx, nil, nil, nil
}
func (s *Store) configIntentionMatchTxn(tx ReadTxn, ws memdb.WatchSet, args *structs.IntentionQueryMatch) (uint64, []structs.Intentions, error) {
maxIndex := uint64(1)
// Make all the calls and accumulate the results
results := make([]structs.Intentions, len(args.Entries))
for i, entry := range args.Entries {
// Note on performance: This is not the most optimal set of queries
// since we repeat some many times (such as */*). We can work on
// improving that in the future, the test cases shouldn't have to
// change for that.
index, ixns, err := configIntentionMatchOneTxn(tx, ws, entry, args.Type, structs.IntentionTargetService)
if err != nil {
return 0, nil, err
}
if index > maxIndex {
maxIndex = index
}
// Store the result
results[i] = ixns
}
return maxIndex, results, nil
}
func configIntentionMatchOneTxn(
tx ReadTxn, ws memdb.WatchSet,
matchEntry structs.IntentionMatchEntry,
matchType structs.IntentionMatchType,
targetType structs.IntentionTargetType,
) (uint64, structs.Intentions, error) {
switch matchType {
// targetType is only relevant for Source matches as egress Destinations can only be Intention Destinations in the mesh
case structs.IntentionMatchSource:
return readSourceIntentionsFromConfigEntriesTxn(tx, ws, matchEntry.Name, matchEntry.GetEnterpriseMeta(), targetType)
case structs.IntentionMatchDestination:
return readDestinationIntentionsFromConfigEntriesTxn(tx, ws, matchEntry.Name, matchEntry.GetEnterpriseMeta())
default:
return 0, nil, fmt.Errorf("invalid intention match type: %s", matchType)
}
}
func readSourceIntentionsFromConfigEntriesTxn(
tx ReadTxn,
ws memdb.WatchSet,
serviceName string,
entMeta *acl.EnterpriseMeta,
targetType structs.IntentionTargetType,
) (uint64, structs.Intentions, error) {
idx := maxIndexTxn(tx, tableConfigEntries)
var (
results structs.Intentions
err error
)
names := getIntentionPrecedenceMatchServiceNames(serviceName, entMeta)
for _, sn := range names {
results, err = readSourceIntentionsFromConfigEntriesForServiceTxn(tx, ws, sn.Name, &sn.EnterpriseMeta, results, targetType)
if err != nil {
return 0, nil, err
}
results, err = readSourceSamenessIntentionsFromConfigEntriesForServiceTxn(tx, ws, sn.Name, &sn.EnterpriseMeta, results, targetType)
if err != nil {
return 0, nil, err
}
}
// Sort the results by precedence
sort.Sort(structs.IntentionPrecedenceSorter(results))
return idx, results, nil
}
func readSourceIntentionsFromConfigEntriesForServiceTxn(
tx ReadTxn,
ws memdb.WatchSet,
serviceName string,
sourceEntMeta *acl.EnterpriseMeta,
results structs.Intentions,
targetType structs.IntentionTargetType,
) (structs.Intentions, error) {
sn := structs.NewServiceName(serviceName, sourceEntMeta)
iter, err := tx.Get(tableConfigEntries, indexSource, sn)
if err != nil {
return nil, fmt.Errorf("failed config entry lookup: %s", err)
}
ws.Add(iter.WatchCh())
for v := iter.Next(); v != nil; v = iter.Next() {
entry := v.(*structs.ServiceIntentionsConfigEntry)
entMeta := entry.DestinationServiceName().EnterpriseMeta
kind, err := serviceIntentionsToGatewayServiceKind(tx, entry.DestinationServiceName().Name, entMeta)
if err != nil {
return nil, err
}
for _, src := range entry.Sources {
if src.SourceServiceName() == sn {
canAdd, err := intentionMatches(targetType, kind, entry.HasWildcardDestination())
if err != nil {
return nil, err
}
if canAdd {
results = append(results, entry.ToIntention(src))
}
}
}
}
return results, nil
}
func serviceIntentionsToGatewayServiceKind(tx ReadTxn, serviceName string, entMeta acl.EnterpriseMeta) (structs.GatewayServiceKind, error) {
var err error
kind := structs.GatewayServiceKindService
// if we have a wildcard namespace or partition assume we are querying a service intention
// as destination intentions will never be queried as wildcard
if entMeta.NamespaceOrDefault() != acl.WildcardName && entMeta.PartitionOrDefault() != acl.WildcardName {
kind, err = GatewayServiceKind(tx, serviceName, &entMeta)
if err != nil {
return kind, err
}
}
return kind, nil
}
func intentionMatches(targetType structs.IntentionTargetType, kind structs.GatewayServiceKind, wildcardDestination bool) (bool, error) {
var canAdd bool
switch targetType {
case structs.IntentionTargetService:
canAdd = kind == structs.GatewayServiceKindService || kind == structs.GatewayServiceKindUnknown
case structs.IntentionTargetDestination:
// wildcard is needed here to be able to consider destinations in the wildcard intentions
canAdd = kind == structs.GatewayServiceKindDestination || wildcardDestination
default:
return false, fmt.Errorf("invalid target type")
}
return canAdd, nil
}
func readDestinationIntentionsFromConfigEntriesTxn(tx ReadTxn, ws memdb.WatchSet, serviceName string, entMeta *acl.EnterpriseMeta) (uint64, structs.Intentions, error) {
idx := maxIndexTxn(tx, tableConfigEntries)
var results structs.Intentions
names := getIntentionPrecedenceMatchServiceNames(serviceName, entMeta)
for _, sn := range names {
_, entry, err := getServiceIntentionsConfigEntryTxn(tx, ws, sn.Name, nil, &sn.EnterpriseMeta)
if err != nil {
return 0, nil, err
} else if entry != nil {
results = append(results, entry.ToIntentions()...)
}
}
// Sort the results by precedence
sort.Sort(structs.IntentionPrecedenceSorter(results))
return idx, results, nil
}