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 }