internal/resource/tree.go (133 lines of code) (raw):
package resource
import (
"encoding/json"
"slices"
apiv1 "github.com/Azure/eno/api/v1"
"github.com/emirpasic/gods/v2/trees/redblacktree"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type indexedResource struct {
Resource *Resource
Seen bool
PendingDependencies map[Ref]struct{}
Dependents map[Ref]*indexedResource
CompositionDeleting bool
}
// treeBuilder is used to index a set of resources into a stateTree.
type treeBuilder struct {
byRef map[Ref]*indexedResource // fast key/value lookup by group/kind/ns/name
byGroup *redblacktree.Tree[int, []*indexedResource] // fast search for sparse readiness groups
byDefiningGK map[schema.GroupKind]*indexedResource // index CRDs by the GK they define
byGK map[schema.GroupKind]*indexedResource // index all resources by their GK
}
func (b *treeBuilder) init() {
if b.byRef == nil {
b.byRef = map[Ref]*indexedResource{}
}
if b.byGroup == nil {
b.byGroup = redblacktree.New[int, []*indexedResource]()
}
if b.byDefiningGK == nil {
b.byDefiningGK = map[schema.GroupKind]*indexedResource{}
}
if b.byGK == nil {
b.byGK = map[schema.GroupKind]*indexedResource{}
}
}
func (b *treeBuilder) Add(resource *Resource) {
b.init()
// Handle conflicting refs deterministically
if existing, ok := b.byRef[resource.Ref]; ok && resource.Less(existing.Resource) {
return
}
// Index the resource into the builder
idx := &indexedResource{
Resource: resource,
PendingDependencies: map[Ref]struct{}{},
Dependents: map[Ref]*indexedResource{},
}
b.byRef[resource.Ref] = idx
current, _ := b.byGroup.Get(resource.ReadinessGroup)
b.byGroup.Put(resource.ReadinessGroup, append(current, idx))
b.byGK[resource.GVK.GroupKind()] = idx
if resource.DefinedGroupKind != nil {
b.byDefiningGK[*resource.DefinedGroupKind] = idx
}
}
func (b *treeBuilder) Build() *tree {
t := &tree{
byRef: b.byRef,
byManiRef: map[ManifestRef]*indexedResource{},
}
for _, idx := range b.byRef {
t.byManiRef[idx.Resource.ManifestRef] = idx
// CRs are dependent on their CRDs
i := b.byGroup.IteratorAt(b.byGroup.GetNode(idx.Resource.ReadinessGroup))
crd, ok := b.byDefiningGK[idx.Resource.GVK.GroupKind()]
if ok {
idx.PendingDependencies[crd.Resource.Ref] = struct{}{}
crd.Dependents[idx.Resource.Ref] = idx
}
// Depend on any resources in the previous readiness group
if i.Prev() {
for _, dep := range i.Value() {
idx.PendingDependencies[dep.Resource.Ref] = struct{}{}
}
}
i.Next() // Prev always moves the cursor, even if it returns false
// Any resources in the next readiness group depend on us
if i.Next() && i.Key() > idx.Resource.ReadinessGroup {
for _, cur := range i.Value() {
idx.Dependents[cur.Resource.Ref] = cur
}
}
}
return t
}
// tree is an optimized, indexed representation of a set of resources.
// NOT CONCURRENCY SAFE.
type tree struct {
byRef map[Ref]*indexedResource
byManiRef map[ManifestRef]*indexedResource
}
// Get returns the resource and determines if it's visible based on the state of its dependencies.
func (t *tree) Get(key Ref) (res *Resource, visible bool, found bool) {
idx, ok := t.byRef[key]
if !ok {
return nil, false, false
}
return idx.Resource, len(idx.PendingDependencies) == 0 || idx.CompositionDeleting, true
}
// UpdateState updates the state of a resource and requeues dependents if necessary.
func (t *tree) UpdateState(comp *apiv1.Composition, ref ManifestRef, state *apiv1.ResourceState, enqueue func(Ref)) {
idx, ok := t.byManiRef[ref]
if !ok {
return
}
// Requeue self when the state has changed
lastKnown := idx.Resource.latestKnownState.Swap(state)
if (!idx.Seen && lastKnown == nil) || !lastKnown.Equal(state) || (!idx.CompositionDeleting && comp.DeletionTimestamp != nil) {
enqueue(idx.Resource.Ref)
}
idx.Seen = true
idx.CompositionDeleting = comp.DeletionTimestamp != nil
// Dependents should no longer be blocked by this resource
if state.Ready != nil && (lastKnown == nil || lastKnown.Ready == nil) {
for _, dep := range idx.Dependents {
delete(dep.PendingDependencies, idx.Resource.Ref)
enqueue(dep.Resource.Ref)
}
}
}
// MarshalJSON allows the current tree to be serialized to JSON for testing/debugging purposes.
// This should not be expected to provide a stable schema.
func (t *tree) MarshalJSON() ([]byte, error) {
tree := map[string]any{}
for key, value := range t.byRef {
dependencies := []string{}
for ref := range value.PendingDependencies {
dependencies = append(dependencies, ref.String())
}
slices.Sort(dependencies)
dependents := []string{}
for ref := range value.Dependents {
dependents = append(dependents, ref.String())
}
slices.Sort(dependents)
state := value.Resource.latestKnownState.Load()
valMap := map[string]any{
"ready": state != nil && state.Ready != nil,
"reconciled": state != nil && state.Reconciled,
"dependencies": dependencies,
"dependents": dependents,
}
tree[key.String()] = valMap
}
return json.Marshal(&tree)
}