github.com/tilt-dev/tilt@v0.36.0/internal/controllers/core/podlogstream/podsource.go (about) 1 package podlogstream 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "time" 8 9 "k8s.io/apimachinery/pkg/runtime" 10 "k8s.io/apimachinery/pkg/runtime/schema" 11 "k8s.io/apimachinery/pkg/types" 12 "k8s.io/client-go/util/workqueue" 13 "sigs.k8s.io/controller-runtime/pkg/client" 14 "sigs.k8s.io/controller-runtime/pkg/reconcile" 15 "sigs.k8s.io/controller-runtime/pkg/source" 16 17 "github.com/jonboulle/clockwork" 18 19 "github.com/tilt-dev/tilt/internal/controllers/indexer" 20 "github.com/tilt-dev/tilt/internal/k8s" 21 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 22 ) 23 24 var podGVK = schema.GroupVersionKind{Version: "v1", Kind: "Pod"} 25 var nsGVK = schema.GroupVersionKind{Version: "v1", Kind: "Namespace"} 26 27 // Helper struct that captures Pod changes and queues up a Reconcile() 28 // call for any PodLogStream watching that pod. 29 type PodSource struct { 30 ctx context.Context 31 indexer *indexer.Indexer 32 kClient k8s.Client 33 q workqueue.TypedRateLimitingInterface[reconcile.Request] 34 clock clockwork.Clock 35 36 watchesByNamespace map[string]*podWatch 37 mu sync.Mutex 38 } 39 40 type podWatch struct { 41 ctx context.Context 42 cancel func() 43 namespace string 44 45 // Only populated if ctx.Err() != nil (the context has been cancelled) 46 finishedAt time.Time 47 error error 48 } 49 50 var _ source.Source = &PodSource{} 51 var _ fmt.Stringer = &PodSource{} 52 53 func NewPodSource(ctx context.Context, kClient k8s.Client, scheme *runtime.Scheme, clock clockwork.Clock) *PodSource { 54 return &PodSource{ 55 ctx: ctx, 56 indexer: indexer.NewIndexer(scheme, indexPodLogStreamForKubernetes), 57 kClient: kClient, 58 watchesByNamespace: make(map[string]*podWatch), 59 clock: clock, 60 } 61 } 62 63 func (s *PodSource) String() string { 64 return "pod-source" 65 } 66 67 func (s *PodSource) Start(ctx context.Context, q workqueue.TypedRateLimitingInterface[reconcile.Request]) error { 68 s.mu.Lock() 69 defer s.mu.Unlock() 70 s.q = q 71 return nil 72 } 73 74 func (s *PodSource) TearDown() { 75 s.mu.Lock() 76 defer s.mu.Unlock() 77 78 for k, pw := range s.watchesByNamespace { 79 pw.cancel() 80 delete(s.watchesByNamespace, k) 81 } 82 } 83 84 // Register the pods for this stream. 85 // 86 // Set up any watches we need. 87 func (s *PodSource) handleReconcileRequest(ctx context.Context, name types.NamespacedName, pls *PodLogStream) error { 88 s.mu.Lock() 89 defer s.mu.Unlock() 90 91 s.indexer.OnReconcile(name, pls) 92 93 var err error 94 ns := pls.Spec.Namespace 95 if ns != "" { 96 pw, ok := s.watchesByNamespace[ns] 97 if !ok { 98 ctx, cancel := context.WithCancel(ctx) 99 pw = &podWatch{ctx: ctx, cancel: cancel, namespace: ns} 100 s.watchesByNamespace[ns] = pw 101 go s.doWatch(pw) 102 } 103 104 if pw.ctx.Err() != nil { 105 err = pw.ctx.Err() 106 if pw.error != nil { 107 err = pw.error 108 } 109 } 110 } 111 return err 112 } 113 114 // Process pod events and make sure they trigger a reconcile. 115 func (s *PodSource) doWatch(pw *podWatch) { 116 defer func() { 117 // If the watch wasn't cancelled and there's no other error, 118 // record a generic error. 119 if pw.error == nil && pw.ctx.Err() == nil { 120 pw.error = fmt.Errorf("watch disconnected") 121 } 122 123 pw.finishedAt = s.clock.Now() 124 pw.cancel() 125 s.requeueIndexerKey(indexer.Key{Name: types.NamespacedName{Name: pw.namespace}, GVK: nsGVK}) 126 }() 127 128 pw.finishedAt = time.Time{} 129 pw.error = nil 130 131 podCh, err := s.kClient.WatchPods(s.ctx, k8s.Namespace(pw.namespace)) 132 if err != nil { 133 pw.error = fmt.Errorf("watching pods: %v", err) 134 return 135 } 136 137 for { 138 select { 139 case <-pw.ctx.Done(): 140 return 141 142 case pod, ok := <-podCh: 143 if !ok { 144 return 145 } 146 s.handlePod(pod) 147 continue 148 } 149 } 150 } 151 152 // Turn all pod events into Reconcile() calls. 153 func (s *PodSource) handlePod(obj k8s.ObjectUpdate) { 154 podNN, ok := obj.AsNamespacedName() 155 if !ok { 156 return 157 } 158 159 s.requeueIndexerKey(indexer.Key{Name: podNN, GVK: podGVK}) 160 } 161 162 func (s *PodSource) requeueIndexerKey(key indexer.Key) { 163 s.mu.Lock() 164 requests := s.indexer.EnqueueKey(key) 165 q := s.q 166 s.mu.Unlock() 167 168 if q == nil { 169 return 170 } 171 172 for _, req := range requests { 173 q.Add(req) 174 } 175 } 176 func (s *PodSource) requeueStream(name types.NamespacedName) { 177 s.mu.Lock() 178 q := s.q 179 s.mu.Unlock() 180 181 if q == nil { 182 return 183 } 184 q.Add(reconcile.Request{NamespacedName: name}) 185 } 186 187 // indexPodLogStreamForKubernetes indexes a PodLogStream object and returns keys 188 // for Pods from the K8s cluster that it watches. 189 // 190 // See also: indexPodLogStreamForTiltAPI which indexes a PodLogStream object 191 // and returns keys for objects from the Tilt apiserver that it watches. 192 func indexPodLogStreamForKubernetes(obj client.Object) []indexer.Key { 193 pls := obj.(*v1alpha1.PodLogStream) 194 if pls.Spec.Pod == "" { 195 return nil 196 } 197 return []indexer.Key{ 198 // Watch events broadcast on the whole namespace. 199 indexer.Key{ 200 Name: types.NamespacedName{Name: pls.Spec.Namespace}, 201 GVK: nsGVK, 202 }, 203 // Watch events on this specific Pod. 204 indexer.Key{ 205 Name: types.NamespacedName{Name: pls.Spec.Pod, Namespace: pls.Spec.Namespace}, 206 GVK: podGVK, 207 }, 208 } 209 }