consul/internal/resource/mappers/selectiontracker/selection_tracker.go

210 lines
6.8 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 13:12:13 +00:00
// SPDX-License-Identifier: BUSL-1.1
package selectiontracker
import (
"context"
"fmt"
"sync"
"golang.org/x/exp/slices"
"github.com/hashicorp/consul/internal/controller"
"github.com/hashicorp/consul/internal/radix"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/lib/stringslice"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v2beta1"
"github.com/hashicorp/consul/proto-public/pbresource"
)
type WorkloadSelectionTracker struct {
lock sync.Mutex
prefixes *radix.Tree[[]*pbresource.ID]
exact *radix.Tree[[]*pbresource.ID]
// workloadSelectors contains a map keyed on resource references with values
// being the selector that resource is currently associated with. This map
// is kept mainly to make tracking removal operations more efficient.
// Generally any operation that could take advantage of knowing where
// in the trees the resource id is referenced can use this to prevent
// needing to search the whole tree.
workloadSelectors map[resource.ReferenceKey]*pbcatalog.WorkloadSelector
}
func New() *WorkloadSelectionTracker {
return &WorkloadSelectionTracker{
prefixes: radix.New[[]*pbresource.ID](),
exact: radix.New[[]*pbresource.ID](),
workloadSelectors: make(map[resource.ReferenceKey]*pbcatalog.WorkloadSelector),
}
}
// MapWorkload will return a slice of controller.Requests with 1 resource for
// each resource that selects the specified Workload resource.
func (t *WorkloadSelectionTracker) MapWorkload(_ context.Context, _ controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) {
resIds := t.GetIDsForWorkload(res.Id)
return controller.MakeRequests(nil, resIds), nil
}
func (t *WorkloadSelectionTracker) GetIDsForWorkload(id *pbresource.ID) []*pbresource.ID {
t.lock.Lock()
defer t.lock.Unlock()
var result []*pbresource.ID
workloadTreeKey := treePathFromNameOrPrefix(id.GetTenancy(), id.GetName())
// gather the list of all resources that select the specified workload using a prefix match
t.prefixes.WalkPath(workloadTreeKey, func(path string, ids []*pbresource.ID) bool {
result = append(result, ids...)
return false
})
// gather the list of all resources that select the specified workload using an exact match
exactReqs, _ := t.exact.Get(workloadTreeKey)
// return the combined list of all resources that select the specified workload
return append(result, exactReqs...)
}
// TrackIDForSelector will associate workloads matching the specified workload
// selector with the given resource id.
func (t *WorkloadSelectionTracker) TrackIDForSelector(id *pbresource.ID, selector *pbcatalog.WorkloadSelector) {
if selector == nil {
return
}
t.lock.Lock()
defer t.lock.Unlock()
ref := resource.NewReferenceKey(id)
if previousSelector, found := t.workloadSelectors[ref]; found {
if stringslice.Equal(previousSelector.Names, selector.Names) &&
stringslice.Equal(previousSelector.Prefixes, selector.Prefixes) {
// the selector is unchanged so do nothing
return
}
// Potentially we could detect differences and do more minimal work. However
// users are not expected to alter workload selectors often and therefore
// not optimizing this further is probably fine. Therefore we are going
// to wipe all tracking of the id and reinsert things.
t.untrackID(id)
}
// loop over all the exact matching rules and associate those workload names
// with the given resource id
for _, name := range selector.GetNames() {
key := treePathFromNameOrPrefix(id.GetTenancy(), name)
// lookup any resource id associations for the given workload name
leaf, _ := t.exact.Get(key)
// append the ID to the existing request list
t.exact.Insert(key, append(leaf, id))
}
// loop over all the prefix matching rules and associate those prefixes
// with the given resource id.
for _, prefix := range selector.GetPrefixes() {
key := treePathFromNameOrPrefix(id.GetTenancy(), prefix)
// lookup any resource id associations for the given workload name prefix
leaf, _ := t.prefixes.Get(key)
// append the new resource ID to the existing request list
t.prefixes.Insert(key, append(leaf, id))
}
t.workloadSelectors[ref] = selector
}
// UntrackID causes the tracker to stop tracking the given resource ID
func (t *WorkloadSelectionTracker) UntrackID(id *pbresource.ID) {
t.lock.Lock()
defer t.lock.Unlock()
t.untrackID(id)
}
// GetSelector returns the currently stored selector for the given ID.
func (t *WorkloadSelectionTracker) GetSelector(id *pbresource.ID) *pbcatalog.WorkloadSelector {
t.lock.Lock()
defer t.lock.Unlock()
return t.workloadSelectors[resource.NewReferenceKey(id)]
}
// untrackID should be called to stop tracking a resource ID.
// This method assumes the lock is already held. Besides modifying
// the prefix & name trees to not reference this ID, it will also
// delete any corresponding entry within the workloadSelectors map
func (t *WorkloadSelectionTracker) untrackID(id *pbresource.ID) {
ref := resource.NewReferenceKey(id)
selector, found := t.workloadSelectors[ref]
if !found {
return
}
exactTreePaths := make([]string, len(selector.GetNames()))
for i, name := range selector.GetNames() {
exactTreePaths[i] = treePathFromNameOrPrefix(id.GetTenancy(), name)
}
prefixTreePaths := make([]string, len(selector.GetPrefixes()))
for i, prefix := range selector.GetPrefixes() {
prefixTreePaths[i] = treePathFromNameOrPrefix(id.GetTenancy(), prefix)
}
removeIDFromTreeAtPaths(t.exact, id, exactTreePaths)
removeIDFromTreeAtPaths(t.prefixes, id, prefixTreePaths)
// If we don't do this deletion then reinsertion of the id for
// tracking in the future could prevent selection criteria from
// being properly inserted into the radix trees.
delete(t.workloadSelectors, ref)
}
// removeIDFromTree will remove the given resource ID from all leaf nodes in the radix tree.
func removeIDFromTreeAtPaths(t *radix.Tree[[]*pbresource.ID], id *pbresource.ID, paths []string) {
for _, path := range paths {
ids, _ := t.Get(path)
foundIdx := -1
for idx, resID := range ids {
if resource.EqualID(resID, id) {
foundIdx = idx
break
}
}
if foundIdx != -1 {
l := len(ids)
if foundIdx == l-1 {
ids = ids[:foundIdx]
} else {
ids = slices.Delete(ids, foundIdx, foundIdx+1)
}
if len(ids) > 0 {
t.Insert(path, ids)
} else {
t.Delete(path)
}
}
}
}
// treePathFromNameOrPrefix computes radix tree key from the resource tenancy and a selector name or prefix.
// The keys will be computed in the following form:
// <partition>/<namespace>/<name or prefix>.
func treePathFromNameOrPrefix(tenancy *pbresource.Tenancy, nameOrPrefix string) string {
// TODO(peering/v2) update paths for peer tenancy
return fmt.Sprintf("%s/%s/%s",
tenancy.GetPartition(),
tenancy.GetNamespace(),
nameOrPrefix)
}