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  }