github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/k8s/owner_fetcher.go (about)

     1  package k8s
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"sync"
     8  
     9  	"golang.org/x/exp/slices"
    10  	v1 "k8s.io/api/core/v1"
    11  	"k8s.io/apimachinery/pkg/api/errors"
    12  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  	"k8s.io/apimachinery/pkg/runtime"
    14  	"k8s.io/apimachinery/pkg/runtime/schema"
    15  	"k8s.io/apimachinery/pkg/types"
    16  
    17  	"github.com/tilt-dev/tilt/pkg/logger"
    18  )
    19  
    20  // The ObjectRefTree only contains immutable properties
    21  // of a Kubernetes object: the name, namespace, and UID
    22  type ObjectRefTree struct {
    23  	Ref               v1.ObjectReference
    24  	CreationTimestamp metav1.Time
    25  	Owners            []ObjectRefTree
    26  }
    27  
    28  func (t ObjectRefTree) UIDs() []types.UID {
    29  	result := []types.UID{t.Ref.UID}
    30  	for _, owner := range t.Owners {
    31  		result = append(result, owner.UIDs()...)
    32  	}
    33  	return result
    34  }
    35  
    36  func (t ObjectRefTree) stringLines() []string {
    37  	result := []string{fmt.Sprintf("%s:%s", t.Ref.Kind, t.Ref.Name)}
    38  	for _, owner := range t.Owners {
    39  		// indent each of the owners by two spaces
    40  		branchLines := owner.stringLines()
    41  		for _, branchLine := range branchLines {
    42  			result = append(result, fmt.Sprintf("  %s", branchLine))
    43  		}
    44  	}
    45  	return result
    46  }
    47  
    48  func (t ObjectRefTree) String() string {
    49  	return strings.Join(t.stringLines(), "\n")
    50  }
    51  
    52  type resourceNamespace struct {
    53  	Namespace Namespace
    54  	GVK       schema.GroupVersionKind
    55  }
    56  
    57  type MetaClient interface {
    58  	GetMetaByReference(ctx context.Context, ref v1.ObjectReference) (metav1.Object, error)
    59  	ListMeta(ctx context.Context, gvk schema.GroupVersionKind, ns Namespace) ([]metav1.Object, error)
    60  	WatchMeta(ctx context.Context, gvk schema.GroupVersionKind, ns Namespace) (<-chan metav1.Object, error)
    61  }
    62  
    63  type OwnerFetcher struct {
    64  	globalCtx context.Context
    65  	cli       MetaClient
    66  	cache     map[types.UID]*objectTreePromise
    67  	mu        *sync.Mutex
    68  
    69  	metaCache       map[types.UID]metav1.Object
    70  	resourceFetches map[resourceNamespace]*sync.Once
    71  }
    72  
    73  func NewOwnerFetcher(ctx context.Context, metaClient MetaClient) OwnerFetcher {
    74  	return OwnerFetcher{
    75  		globalCtx: ctx,
    76  		cli:       metaClient,
    77  		cache:     make(map[types.UID]*objectTreePromise),
    78  		mu:        &sync.Mutex{},
    79  
    80  		metaCache:       make(map[types.UID]metav1.Object),
    81  		resourceFetches: make(map[resourceNamespace]*sync.Once),
    82  	}
    83  }
    84  
    85  func (v OwnerFetcher) getOrCreateResourceFetch(gvk schema.GroupVersionKind, ns Namespace) *sync.Once {
    86  	v.mu.Lock()
    87  	defer v.mu.Unlock()
    88  	rns := resourceNamespace{Namespace: ns, GVK: gvk}
    89  	fetch, ok := v.resourceFetches[rns]
    90  	if !ok {
    91  		fetch = &sync.Once{}
    92  		v.resourceFetches[rns] = fetch
    93  	}
    94  	return fetch
    95  }
    96  
    97  // As an optimization, we batch fetch all the ObjectMetas of a resource type
    98  // the first time we need that resource, then watch updates.
    99  func (v OwnerFetcher) ensureResourceFetched(gvk schema.GroupVersionKind, ns Namespace) {
   100  	fetch := v.getOrCreateResourceFetch(gvk, ns)
   101  	fetch.Do(func() {
   102  		metas, err := v.cli.ListMeta(v.globalCtx, gvk, ns)
   103  		if err != nil {
   104  			logger.Get(v.globalCtx).Debugf("Error fetching metadata: %v", err)
   105  			return
   106  		}
   107  
   108  		v.mu.Lock()
   109  		for _, meta := range metas {
   110  			v.metaCache[meta.GetUID()] = meta
   111  		}
   112  		v.mu.Unlock()
   113  
   114  		ch, err := v.cli.WatchMeta(v.globalCtx, gvk, ns)
   115  		if err != nil {
   116  			logger.Get(v.globalCtx).Debugf("Error watching metadata: %v", err)
   117  			return
   118  		}
   119  
   120  		go func() {
   121  			for meta := range ch {
   122  				// NOTE(nick): I don't think we can ever get a blank UID, but want to protect
   123  				// us from weird k8s bugs.
   124  				if meta.GetUID() == "" {
   125  					continue
   126  				}
   127  
   128  				v.mu.Lock()
   129  				v.metaCache[meta.GetUID()] = meta
   130  				v.mu.Unlock()
   131  			}
   132  		}()
   133  	})
   134  }
   135  
   136  // Returns a promise and a boolean. The boolean is true if the promise is
   137  // already in progress, and false if the caller is responsible for
   138  // resolving/rejecting the promise.
   139  func (v OwnerFetcher) getOrCreatePromise(id types.UID) (*objectTreePromise, bool) {
   140  	v.mu.Lock()
   141  	defer v.mu.Unlock()
   142  	promise, ok := v.cache[id]
   143  	if !ok {
   144  		promise = newObjectTreePromise()
   145  		v.cache[id] = promise
   146  	}
   147  	return promise, ok
   148  }
   149  
   150  func (v OwnerFetcher) OwnerTreeOfRef(ctx context.Context, ref v1.ObjectReference) (result ObjectRefTree, err error) {
   151  	return v.ownerTreeOfRefHelper(ctx, ref, nil)
   152  }
   153  
   154  func (v OwnerFetcher) ownerTreeOfRefHelper(ctx context.Context, ref v1.ObjectReference, path []types.UID) (result ObjectRefTree, err error) {
   155  	uid := ref.UID
   156  	if uid == "" {
   157  		return ObjectRefTree{}, fmt.Errorf("Can only get owners of deployed entities")
   158  	}
   159  
   160  	promise, ok := v.getOrCreatePromise(uid)
   161  	if ok {
   162  		return promise.wait()
   163  	}
   164  
   165  	defer func() {
   166  		if err != nil {
   167  			promise.reject(err)
   168  		} else {
   169  			promise.resolve(result)
   170  		}
   171  	}()
   172  
   173  	meta, err := v.getMetaByReference(ctx, ref)
   174  	if err != nil {
   175  		if errors.IsNotFound(err) {
   176  			return ObjectRefTree{Ref: ref}, nil
   177  		}
   178  		return ObjectRefTree{}, err
   179  	}
   180  	return v.ownerTreeOfHelper(ctx, ref, meta, path)
   181  }
   182  
   183  func (v OwnerFetcher) getMetaByReference(ctx context.Context, ref v1.ObjectReference) (metav1.Object, error) {
   184  	gvk := ReferenceGVK(ref)
   185  	v.ensureResourceFetched(gvk, Namespace(ref.Namespace))
   186  
   187  	v.mu.Lock()
   188  	meta, ok := v.metaCache[ref.UID]
   189  	v.mu.Unlock()
   190  
   191  	if ok {
   192  		return meta, nil
   193  	}
   194  
   195  	return v.cli.GetMetaByReference(ctx, ref)
   196  }
   197  
   198  func (v OwnerFetcher) OwnerTreeOf(ctx context.Context, entity K8sEntity) (result ObjectRefTree, err error) {
   199  	meta := entity.Meta()
   200  	uid := meta.GetUID()
   201  	if uid == "" {
   202  		return ObjectRefTree{}, fmt.Errorf("Can only get owners of deployed entities")
   203  	}
   204  
   205  	promise, ok := v.getOrCreatePromise(uid)
   206  	if ok {
   207  		return promise.wait()
   208  	}
   209  
   210  	defer func() {
   211  		if err != nil {
   212  			promise.reject(err)
   213  		} else {
   214  			promise.resolve(result)
   215  		}
   216  	}()
   217  
   218  	ref := entity.ToObjectReference()
   219  	return v.ownerTreeOfHelper(ctx, ref, meta, nil)
   220  }
   221  
   222  func (v OwnerFetcher) ownerTreeOfHelper(ctx context.Context, ref v1.ObjectReference, meta metav1.Object, path []types.UID) (ObjectRefTree, error) {
   223  	tree := ObjectRefTree{Ref: ref, CreationTimestamp: meta.GetCreationTimestamp()}
   224  	owners := meta.GetOwnerReferences()
   225  	for _, owner := range owners {
   226  		// TODO: Owner references can also exist at cluster scope, for which this incorrectly propagates the parent ref's Namespace.
   227  		ownerRef := OwnerRefToObjectRef(owner, meta.GetNamespace())
   228  		if slices.Contains(path, owner.UID) {
   229  			// break circular dependencies
   230  			continue
   231  		}
   232  		ownerTree, err := v.ownerTreeOfRefHelper(ctx, ownerRef, append(path, ref.UID))
   233  		if err != nil {
   234  			return ObjectRefTree{}, err
   235  		}
   236  		tree.Owners = append(tree.Owners, ownerTree)
   237  	}
   238  	return tree, nil
   239  }
   240  
   241  func OwnerRefToObjectRef(owner metav1.OwnerReference, namespace string) v1.ObjectReference {
   242  	return v1.ObjectReference{
   243  		APIVersion: owner.APIVersion,
   244  		Kind:       owner.Kind,
   245  		Namespace:  namespace,
   246  		Name:       owner.Name,
   247  		UID:        owner.UID,
   248  	}
   249  }
   250  
   251  func RuntimeObjToOwnerRef(obj runtime.Object) metav1.OwnerReference {
   252  	e := NewK8sEntity(obj)
   253  	ref := e.ToObjectReference()
   254  	return metav1.OwnerReference{
   255  		APIVersion: ref.APIVersion,
   256  		Kind:       ref.Kind,
   257  		Name:       ref.Name,
   258  		UID:        ref.UID,
   259  	}
   260  }
   261  
   262  type objectTreePromise struct {
   263  	tree ObjectRefTree
   264  	err  error
   265  	done chan struct{}
   266  }
   267  
   268  func newObjectTreePromise() *objectTreePromise {
   269  	return &objectTreePromise{
   270  		done: make(chan struct{}),
   271  	}
   272  }
   273  
   274  func (e *objectTreePromise) resolve(tree ObjectRefTree) {
   275  	e.tree = tree
   276  	close(e.done)
   277  }
   278  
   279  func (e *objectTreePromise) reject(err error) {
   280  	e.err = err
   281  	close(e.done)
   282  }
   283  
   284  func (e *objectTreePromise) wait() (ObjectRefTree, error) {
   285  	<-e.done
   286  	return e.tree, e.err
   287  }