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 }