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  }