github.com/grahambrereton-form3/tilt@v0.10.18/internal/k8s/watch.go (about)

     1  package k8s
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"time"
     8  
     9  	"github.com/pkg/errors"
    10  	v1 "k8s.io/api/core/v1"
    11  	apiErrors "k8s.io/apimachinery/pkg/api/errors"
    12  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  	"k8s.io/apimachinery/pkg/labels"
    14  	"k8s.io/apimachinery/pkg/runtime/schema"
    15  	"k8s.io/apimachinery/pkg/watch"
    16  	"k8s.io/client-go/informers"
    17  	"k8s.io/client-go/tools/cache"
    18  )
    19  
    20  type watcherFactory func(namespace string) watcher
    21  type watcher interface {
    22  	Watch(options metav1.ListOptions) (watch.Interface, error)
    23  }
    24  
    25  func (kCli K8sClient) makeWatcher(f watcherFactory, ls labels.Selector) (watch.Interface, Namespace, error) {
    26  	// passing "" gets us all namespaces
    27  	w := f("")
    28  	if w == nil {
    29  		return nil, "", nil
    30  	}
    31  
    32  	watcher, err := w.Watch(metav1.ListOptions{LabelSelector: ls.String()})
    33  	if err == nil {
    34  		return watcher, "", nil
    35  	}
    36  
    37  	// If the request failed, we might be able to recover.
    38  	statusErr, isStatusErr := err.(*apiErrors.StatusError)
    39  	if !isStatusErr {
    40  		return nil, "", err
    41  	}
    42  
    43  	status := statusErr.ErrStatus
    44  	if status.Code == http.StatusForbidden {
    45  		// If this is a forbidden error, maybe the user just isn't allowed to watch this namespace.
    46  		// Let's narrow our request to just the config namespace, and see if that helps.
    47  		w := f(kCli.configNamespace.String())
    48  		if w == nil {
    49  			return nil, "", nil
    50  		}
    51  
    52  		watcher, err := w.Watch(metav1.ListOptions{LabelSelector: ls.String()})
    53  		if err == nil {
    54  			return watcher, kCli.configNamespace, nil
    55  		}
    56  
    57  		// ugh, it still failed. return the original error.
    58  	}
    59  	return nil, "", fmt.Errorf("%s, Reason: %s, Code: %d", status.Message, status.Reason, status.Code)
    60  }
    61  
    62  func (kCli K8sClient) makeInformer(
    63  	ctx context.Context,
    64  	gvr schema.GroupVersionResource,
    65  	ls labels.Selector) (cache.SharedInformer, error) {
    66  	// HACK(dmiller): There's no way to get errors out of an informer. See https://github.com/kubernetes/client-go/issues/155
    67  	// In the meantime, at least to get authorization and some other errors let's try to set up a watcher and then just
    68  	// throw it away.
    69  	watcher, ns, err := kCli.makeWatcher(func(ns string) watcher {
    70  		return kCli.dynamic.Resource(gvr).Namespace(ns)
    71  	}, ls)
    72  	if err != nil {
    73  		return nil, errors.Wrap(err, "makeInformer")
    74  	}
    75  	watcher.Stop()
    76  
    77  	options := []informers.SharedInformerOption{}
    78  	if !ls.Empty() {
    79  		options = append(options, informers.WithTweakListOptions(func(o *metav1.ListOptions) {
    80  			o.LabelSelector = ls.String()
    81  		}))
    82  	}
    83  	if ns != "" {
    84  		options = append(options, informers.WithNamespace(ns.String()))
    85  	}
    86  
    87  	factory := informers.NewSharedInformerFactoryWithOptions(kCli.clientset, 5*time.Second, options...)
    88  	resFactory, err := factory.ForResource(gvr)
    89  	if err != nil {
    90  		return nil, errors.Wrap(err, "makeInformer")
    91  	}
    92  	return resFactory.Informer(), nil
    93  }
    94  
    95  func (kCli K8sClient) WatchEvents(ctx context.Context) (<-chan *v1.Event, error) {
    96  	gvr := v1.SchemeGroupVersion.WithResource("events")
    97  	informer, err := kCli.makeInformer(ctx, gvr, labels.Everything())
    98  	if err != nil {
    99  		return nil, errors.Wrap(err, "WatchEvents")
   100  	}
   101  
   102  	ch := make(chan *v1.Event)
   103  	informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
   104  		AddFunc: func(obj interface{}) {
   105  			mObj, ok := obj.(*v1.Event)
   106  			if ok {
   107  				ch <- mObj
   108  			}
   109  		},
   110  		UpdateFunc: func(oldObj interface{}, newObj interface{}) {
   111  			mObj, ok := newObj.(*v1.Event)
   112  			if ok {
   113  				oldObj, ok := oldObj.(*v1.Event)
   114  				// the informer regularly gives us updates for events where cmp.Equal(oldObj, newObj) returns true.
   115  				// we have not investigated why it does this, but these updates seem to always be spurious and
   116  				// uninteresting.
   117  				// we could check cmp.Equal here, but really, `Count` is probably the only reason we even care about
   118  				// updates at all.
   119  				if !ok || oldObj.Count < mObj.Count {
   120  					ch <- mObj
   121  				}
   122  			}
   123  		},
   124  	})
   125  
   126  	go informer.Run(ctx.Done())
   127  
   128  	return ch, nil
   129  }
   130  
   131  func (kCli K8sClient) WatchPods(ctx context.Context, ls labels.Selector) (<-chan *v1.Pod, error) {
   132  	gvr := v1.SchemeGroupVersion.WithResource("pods")
   133  	informer, err := kCli.makeInformer(ctx, gvr, ls)
   134  	if err != nil {
   135  		return nil, errors.Wrap(err, "WatchPods")
   136  	}
   137  
   138  	ch := make(chan *v1.Pod)
   139  	informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
   140  		AddFunc: func(obj interface{}) {
   141  			mObj, ok := obj.(*v1.Pod)
   142  			if ok {
   143  				FixContainerStatusImages(mObj)
   144  				ch <- mObj
   145  			}
   146  		},
   147  		DeleteFunc: func(obj interface{}) {
   148  			mObj, ok := obj.(*v1.Pod)
   149  			if ok {
   150  				FixContainerStatusImages(mObj)
   151  				ch <- mObj
   152  			}
   153  		},
   154  		UpdateFunc: func(oldObj interface{}, newObj interface{}) {
   155  			oldPod, ok := oldObj.(*v1.Pod)
   156  			if !ok {
   157  				return
   158  			}
   159  
   160  			newPod, ok := newObj.(*v1.Pod)
   161  			if !ok {
   162  				return
   163  			}
   164  
   165  			if oldPod != newPod {
   166  				FixContainerStatusImages(newPod)
   167  				ch <- newPod
   168  			}
   169  		},
   170  	})
   171  
   172  	go informer.Run(ctx.Done())
   173  
   174  	return ch, nil
   175  }
   176  
   177  func (kCli K8sClient) WatchServices(ctx context.Context, ls labels.Selector) (<-chan *v1.Service, error) {
   178  	gvr := v1.SchemeGroupVersion.WithResource("services")
   179  	informer, err := kCli.makeInformer(ctx, gvr, ls)
   180  	if err != nil {
   181  		return nil, errors.Wrap(err, "WatchServices")
   182  	}
   183  
   184  	ch := make(chan *v1.Service)
   185  	informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
   186  		AddFunc: func(obj interface{}) {
   187  			mObj, ok := obj.(*v1.Service)
   188  			if ok {
   189  				ch <- mObj
   190  			}
   191  		},
   192  		UpdateFunc: func(oldObj interface{}, newObj interface{}) {
   193  			newService, ok := newObj.(*v1.Service)
   194  			if ok {
   195  				ch <- newService
   196  			}
   197  		},
   198  	})
   199  
   200  	go informer.Run(ctx.Done())
   201  
   202  	return ch, nil
   203  }