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  }