consul/internal/resource/mappers/bimapper/bimapper.go

213 lines
6.0 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package bimapper
import (
"context"
"fmt"
"sync"
"github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/proto-public/pbresource"
)
// Mapper tracks bidirectional lookup for an item that contains references to
// other items. For example: an HTTPRoute has many references to Services.
//
// The primary object is called the "item" and an item has many "links".
// Tracking is done on items.
type Mapper struct {
itemType, linkType *pbresource.Type
lock sync.Mutex
itemToLink map[resource.ReferenceKey]map[resource.ReferenceKey]struct{}
linkToItem map[resource.ReferenceKey]map[resource.ReferenceKey]struct{}
}
// New creates a bimapper between the two required provided types.
func New(itemType, linkType *pbresource.Type) *Mapper {
if itemType == nil {
panic("itemType is required")
}
if linkType == nil {
panic("linkType is required")
}
return &Mapper{
itemType: itemType,
linkType: linkType,
itemToLink: make(map[resource.ReferenceKey]map[resource.ReferenceKey]struct{}),
linkToItem: make(map[resource.ReferenceKey]map[resource.ReferenceKey]struct{}),
}
}
// Reset clears the internal mappings.
func (m *Mapper) Reset() {
m.lock.Lock()
defer m.lock.Unlock()
m.itemToLink = make(map[resource.ReferenceKey]map[resource.ReferenceKey]struct{})
m.linkToItem = make(map[resource.ReferenceKey]map[resource.ReferenceKey]struct{})
}
// IsEmpty returns true if the internal structures are empty.
func (m *Mapper) IsEmpty() bool {
m.lock.Lock()
defer m.lock.Unlock()
return len(m.itemToLink) == 0 && len(m.linkToItem) == 0
}
// UntrackItem removes tracking for the provided item. The item type MUST match
// the type configured for the item.
func (m *Mapper) UntrackItem(item *pbresource.ID) {
if !resource.EqualType(item.Type, m.itemType) {
panic(fmt.Sprintf("expected item type %q got %q",
resource.TypeToString(m.itemType),
resource.TypeToString(item.Type),
))
}
m.untrackItem(resource.NewReferenceKey(item))
}
func (m *Mapper) untrackItem(item resource.ReferenceKey) {
m.lock.Lock()
defer m.lock.Unlock()
m.removeItemLocked(item)
}
// TrackItem adds tracking for the provided item. The item and link types MUST
// match the types configured for the items and links.
func (m *Mapper) TrackItem(item *pbresource.ID, links []*pbresource.Reference) {
if !resource.EqualType(item.Type, m.itemType) {
panic(fmt.Sprintf("expected item type %q got %q",
resource.TypeToString(m.itemType),
resource.TypeToString(item.Type),
))
}
linksAsKeys := make([]resource.ReferenceKey, 0, len(links))
for _, link := range links {
if !resource.EqualType(link.Type, m.linkType) {
panic(fmt.Sprintf("expected link type %q got %q",
resource.TypeToString(m.linkType),
resource.TypeToString(link.Type),
))
}
linksAsKeys = append(linksAsKeys, resource.NewReferenceKey(link))
}
m.trackItem(resource.NewReferenceKey(item), linksAsKeys)
}
func (m *Mapper) trackItem(item resource.ReferenceKey, links []resource.ReferenceKey) {
m.lock.Lock()
defer m.lock.Unlock()
m.removeItemLocked(item)
m.addItemLocked(item, links)
}
// you must hold the lock before calling this function
func (m *Mapper) removeItemLocked(item resource.ReferenceKey) {
for link := range m.itemToLink[item] {
delete(m.linkToItem[link], item)
if len(m.linkToItem[link]) == 0 {
delete(m.linkToItem, link)
}
}
delete(m.itemToLink, item)
}
// you must hold the lock before calling this function
func (m *Mapper) addItemLocked(item resource.ReferenceKey, links []resource.ReferenceKey) {
if m.itemToLink[item] == nil {
m.itemToLink[item] = make(map[resource.ReferenceKey]struct{})
}
for _, link := range links {
m.itemToLink[item][link] = struct{}{}
if m.linkToItem[link] == nil {
m.linkToItem[link] = make(map[resource.ReferenceKey]struct{})
}
m.linkToItem[link][item] = struct{}{}
}
}
// LinksForItem returns references to links related to the requested item.
func (m *Mapper) LinksForItem(item *pbresource.ID) []*pbresource.Reference {
if !resource.EqualType(item.Type, m.itemType) {
panic(fmt.Sprintf("expected item type %q got %q",
resource.TypeToString(m.itemType),
resource.TypeToString(item.Type),
))
}
m.lock.Lock()
defer m.lock.Unlock()
links, ok := m.itemToLink[resource.NewReferenceKey(item)]
if !ok {
return nil
}
out := make([]*pbresource.Reference, 0, len(links))
for link := range links {
out = append(out, link.ToReference())
}
return out
}
// ItemsForLink returns item ids for items related to the provided link.
func (m *Mapper) ItemsForLink(link *pbresource.ID) []*pbresource.ID {
if !resource.EqualType(link.Type, m.linkType) {
panic(fmt.Sprintf("expected type %q got %q",
resource.TypeToString(m.linkType),
resource.TypeToString(link.Type),
))
}
return m.itemsByLink(resource.NewReferenceKey(link))
}
// MapLink is suitable as a DependencyMapper to map the provided link event to its item.
func (m *Mapper) MapLink(_ context.Context, _ controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) {
link := res.Id
if !resource.EqualType(link.Type, m.linkType) {
return nil, fmt.Errorf("expected type %q got %q",
resource.TypeToString(m.linkType),
resource.TypeToString(link.Type),
)
}
itemIDs := m.itemsByLink(resource.NewReferenceKey(link))
out := make([]controller.Request, 0, len(itemIDs))
for _, item := range itemIDs {
if !resource.EqualType(item.Type, m.itemType) {
return nil, fmt.Errorf("expected type %q got %q",
resource.TypeToString(m.itemType),
resource.TypeToString(item.Type),
)
}
out = append(out, controller.Request{ID: item})
}
return out, nil
}
func (m *Mapper) itemsByLink(link resource.ReferenceKey) []*pbresource.ID {
m.lock.Lock()
defer m.lock.Unlock()
items, ok := m.linkToItem[link]
if !ok {
return nil
}
out := make([]*pbresource.ID, 0, len(items))
for item := range items {
out = append(out, item.ToID())
}
return out
}