github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/engine/k8swatch/service_watch.go (about) 1 package k8swatch 2 3 import ( 4 "context" 5 "sync" 6 7 "github.com/pkg/errors" 8 v1 "k8s.io/api/core/v1" 9 "k8s.io/apimachinery/pkg/types" 10 11 "github.com/tilt-dev/tilt/internal/controllers/apis/cluster" 12 "github.com/tilt-dev/tilt/internal/k8s" 13 "github.com/tilt-dev/tilt/internal/store" 14 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 15 "github.com/tilt-dev/tilt/pkg/logger" 16 "github.com/tilt-dev/tilt/pkg/model" 17 ) 18 19 type ServiceWatcher struct { 20 clients *cluster.ClientManager 21 clientKey watcherClientKey 22 23 mu sync.RWMutex 24 watcherKnownState watcherKnownState 25 knownServices map[clusterUID]*v1.Service 26 } 27 28 func NewServiceWatcher(clients cluster.ClientProvider, cfgNS k8s.Namespace) *ServiceWatcher { 29 return &ServiceWatcher{ 30 clients: cluster.NewClientManager(clients), 31 clientKey: watcherClientKey{name: "services"}, 32 watcherKnownState: newWatcherKnownState(cfgNS), 33 knownServices: make(map[clusterUID]*v1.Service), 34 } 35 } 36 37 func (w *ServiceWatcher) diff(st store.RStore) watcherTaskList { 38 state := st.RLockState() 39 defer st.RUnlockState() 40 41 return w.watcherKnownState.createTaskList(state) 42 } 43 44 func (w *ServiceWatcher) OnChange(ctx context.Context, st store.RStore, summary store.ChangeSummary) error { 45 if summary.IsLogOnly() { 46 return nil 47 } 48 49 w.mu.Lock() 50 defer w.mu.Unlock() 51 52 clusters := w.handleClusterChanges(st, summary) 53 54 taskList := w.diff(st) 55 56 for _, teardown := range taskList.teardownNamespaces { 57 watcher, ok := w.watcherKnownState.namespaceWatches[teardown] 58 if ok { 59 watcher.cancel() 60 } 61 delete(w.watcherKnownState.namespaceWatches, teardown) 62 } 63 64 for _, setup := range taskList.setupNamespaces { 65 w.setupWatch(ctx, st, clusters, setup) 66 } 67 68 if len(taskList.newUIDs) > 0 { 69 w.setupNewUIDs(ctx, st, clusters, taskList.newUIDs) 70 } 71 72 return nil 73 } 74 75 func (w *ServiceWatcher) handleClusterChanges(st store.RStore, summary store.ChangeSummary) map[types.NamespacedName]*v1alpha1.Cluster { 76 clusters := make(map[types.NamespacedName]*v1alpha1.Cluster) 77 state := st.RLockState() 78 for k, v := range state.Clusters { 79 clusters[types.NamespacedName{Name: k}] = v.DeepCopy() 80 } 81 st.RUnlockState() 82 83 for clusterNN := range summary.Clusters.Changes { 84 c := clusters[clusterNN] 85 if c != nil && !w.clients.Refresh(w.clientKey, c) { 86 // cluster config didn't change 87 continue 88 } 89 90 // cluster config changed, remove all state so it can be re-built 91 for key := range w.knownServices { 92 if key.cluster == clusterNN { 93 delete(w.knownServices, key) 94 } 95 } 96 97 w.watcherKnownState.resetStateForCluster(clusterNN) 98 } 99 100 return clusters 101 } 102 103 func (w *ServiceWatcher) setupWatch(ctx context.Context, st store.RStore, clusters map[types.NamespacedName]*v1alpha1.Cluster, key clusterNamespace) { 104 kCli, err := w.clients.GetK8sClient(w.clientKey, clusters[key.cluster]) 105 if err != nil { 106 // ignore errors, if the cluster status changes, the subscriber 107 // will be re-run and the namespaces will be picked up again as new 108 // since watcherKnownState isn't updated 109 return 110 } 111 112 ch, err := kCli.WatchServices(ctx, key.namespace) 113 if err != nil { 114 err = errors.Wrapf(err, "Error watching services. Are you connected to kubernetes?\nTry running `kubectl get services -n %q`", key.namespace) 115 st.Dispatch(store.NewErrorAction(err)) 116 return 117 } 118 119 ctx, cancel := context.WithCancel(ctx) 120 w.watcherKnownState.namespaceWatches[key] = namespaceWatch{cancel: cancel} 121 122 go w.dispatchServiceChangesLoop(ctx, kCli, key.cluster, ch, st) 123 } 124 125 // When new UIDs are deployed, go through all our known services and dispatch 126 // new events. This handles the case where we get the Service change event 127 // before the deploy id shows up in the manifest, which is way more common than 128 // you would think. 129 func (w *ServiceWatcher) setupNewUIDs(ctx context.Context, st store.RStore, clusters map[types.NamespacedName]*v1alpha1.Cluster, newUIDs map[clusterUID]model.ManifestName) { 130 for uid, mn := range newUIDs { 131 kCli, err := w.clients.GetK8sClient(w.clientKey, clusters[uid.cluster]) 132 if err != nil { 133 // ignore errors, if the cluster status changes, the subscriber 134 // will be re-run and the namespaces will be picked up again as new 135 // since watcherKnownState isn't updated 136 continue 137 } 138 139 w.watcherKnownState.knownDeployedUIDs[uid] = mn 140 141 service, ok := w.knownServices[uid] 142 if !ok { 143 continue 144 } 145 146 err = DispatchServiceChange(st, service, mn, kCli.NodeIP(ctx)) 147 if err != nil { 148 logger.Get(ctx).Infof("error resolving service url %s: %v", service.Name, err) 149 } 150 } 151 } 152 153 // Match up the service update to a manifest. 154 // 155 // The division between triageServiceUpdate and recordServiceUpdate is a bit artificial, 156 // but is designed this way to be consistent with PodWatcher and EventWatchManager. 157 func (w *ServiceWatcher) triageServiceUpdate(clusterNN types.NamespacedName, service *v1.Service) model.ManifestName { 158 w.mu.Lock() 159 defer w.mu.Unlock() 160 161 uid := clusterUID{cluster: clusterNN, uid: service.UID} 162 w.knownServices[uid] = service 163 164 manifestName, ok := w.watcherKnownState.knownDeployedUIDs[uid] 165 if !ok { 166 return "" 167 } 168 169 return manifestName 170 } 171 172 func (w *ServiceWatcher) dispatchServiceChangesLoop(ctx context.Context, kCli k8s.Client, clusterNN types.NamespacedName, ch <-chan *v1.Service, st store.RStore) { 173 for { 174 select { 175 case service, ok := <-ch: 176 if !ok { 177 return 178 } 179 180 manifestName := w.triageServiceUpdate(clusterNN, service) 181 if manifestName == "" { 182 continue 183 } 184 185 err := DispatchServiceChange(st, service, manifestName, kCli.NodeIP(ctx)) 186 if err != nil { 187 logger.Get(ctx).Infof("error resolving service url %s: %v", service.Name, err) 188 } 189 case <-ctx.Done(): 190 return 191 } 192 } 193 } 194 195 func DispatchServiceChange(st store.RStore, service *v1.Service, mn model.ManifestName, ip k8s.NodeIP) error { 196 url, err := k8s.ServiceURL(service, ip) 197 if err != nil { 198 return err 199 } 200 201 st.Dispatch(NewServiceChangeAction(service, mn, url)) 202 return nil 203 }