github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/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.RateLimitingInterface 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 52 func NewPodSource(ctx context.Context, kClient k8s.Client, scheme *runtime.Scheme, clock clockwork.Clock) *PodSource { 53 return &PodSource{ 54 ctx: ctx, 55 indexer: indexer.NewIndexer(scheme, indexPodLogStreamForKubernetes), 56 kClient: kClient, 57 watchesByNamespace: make(map[string]*podWatch), 58 clock: clock, 59 } 60 } 61 62 func (s *PodSource) Start(ctx context.Context, q workqueue.RateLimitingInterface) error { 63 s.mu.Lock() 64 defer s.mu.Unlock() 65 s.q = q 66 return nil 67 } 68 69 func (s *PodSource) TearDown() { 70 s.mu.Lock() 71 defer s.mu.Unlock() 72 73 for k, pw := range s.watchesByNamespace { 74 pw.cancel() 75 delete(s.watchesByNamespace, k) 76 } 77 } 78 79 // Register the pods for this stream. 80 // 81 // Set up any watches we need. 82 func (s *PodSource) handleReconcileRequest(ctx context.Context, name types.NamespacedName, pls *PodLogStream) error { 83 s.mu.Lock() 84 defer s.mu.Unlock() 85 86 s.indexer.OnReconcile(name, pls) 87 88 var err error 89 ns := pls.Spec.Namespace 90 if ns != "" { 91 pw, ok := s.watchesByNamespace[ns] 92 if !ok { 93 ctx, cancel := context.WithCancel(ctx) 94 pw = &podWatch{ctx: ctx, cancel: cancel, namespace: ns} 95 s.watchesByNamespace[ns] = pw 96 go s.doWatch(pw) 97 } 98 99 if pw.ctx.Err() != nil { 100 err = pw.ctx.Err() 101 if pw.error != nil { 102 err = pw.error 103 } 104 } 105 } 106 return err 107 } 108 109 // Process pod events and make sure they trigger a reconcile. 110 func (s *PodSource) doWatch(pw *podWatch) { 111 defer func() { 112 // If the watch wasn't cancelled and there's no other error, 113 // record a generic error. 114 if pw.error == nil && pw.ctx.Err() == nil { 115 pw.error = fmt.Errorf("watch disconnected") 116 } 117 118 pw.finishedAt = s.clock.Now() 119 pw.cancel() 120 s.requeueIndexerKey(indexer.Key{Name: types.NamespacedName{Name: pw.namespace}, GVK: nsGVK}) 121 }() 122 123 pw.finishedAt = time.Time{} 124 pw.error = nil 125 126 podCh, err := s.kClient.WatchPods(s.ctx, k8s.Namespace(pw.namespace)) 127 if err != nil { 128 pw.error = fmt.Errorf("watching pods: %v", err) 129 return 130 } 131 132 for { 133 select { 134 case <-pw.ctx.Done(): 135 return 136 137 case pod, ok := <-podCh: 138 if !ok { 139 return 140 } 141 s.handlePod(pod) 142 continue 143 } 144 } 145 } 146 147 // Turn all pod events into Reconcile() calls. 148 func (s *PodSource) handlePod(obj k8s.ObjectUpdate) { 149 podNN, ok := obj.AsNamespacedName() 150 if !ok { 151 return 152 } 153 154 s.requeueIndexerKey(indexer.Key{Name: podNN, GVK: podGVK}) 155 } 156 157 func (s *PodSource) requeueIndexerKey(key indexer.Key) { 158 s.mu.Lock() 159 requests := s.indexer.EnqueueKey(key) 160 q := s.q 161 s.mu.Unlock() 162 163 if q == nil { 164 return 165 } 166 167 for _, req := range requests { 168 q.Add(req) 169 } 170 } 171 func (s *PodSource) requeueStream(name types.NamespacedName) { 172 s.mu.Lock() 173 q := s.q 174 s.mu.Unlock() 175 176 if q == nil { 177 return 178 } 179 q.Add(reconcile.Request{NamespacedName: name}) 180 } 181 182 // indexPodLogStreamForKubernetes indexes a PodLogStream object and returns keys 183 // for Pods from the K8s cluster that it watches. 184 // 185 // See also: indexPodLogStreamForTiltAPI which indexes a PodLogStream object 186 // and returns keys for objects from the Tilt apiserver that it watches. 187 func indexPodLogStreamForKubernetes(obj client.Object) []indexer.Key { 188 pls := obj.(*v1alpha1.PodLogStream) 189 if pls.Spec.Pod == "" { 190 return nil 191 } 192 return []indexer.Key{ 193 // Watch events broadcast on the whole namespace. 194 indexer.Key{ 195 Name: types.NamespacedName{Name: pls.Spec.Namespace}, 196 GVK: nsGVK, 197 }, 198 // Watch events on this specific Pod. 199 indexer.Key{ 200 Name: types.NamespacedName{Name: pls.Spec.Pod, Namespace: pls.Spec.Namespace}, 201 GVK: podGVK, 202 }, 203 } 204 }