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

     1  package engine
     2  
     3  import (
     4  	"context"
     5  	"time"
     6  
     7  	v1 "k8s.io/api/core/v1"
     8  	"k8s.io/apimachinery/pkg/util/wait"
     9  
    10  	"github.com/google/go-cmp/cmp"
    11  
    12  	"github.com/windmilleng/tilt/internal/engine/runtimelog"
    13  	"github.com/windmilleng/tilt/internal/k8s"
    14  	"github.com/windmilleng/tilt/internal/store"
    15  	"github.com/windmilleng/tilt/pkg/logger"
    16  	"github.com/windmilleng/tilt/pkg/model"
    17  )
    18  
    19  type PortForwardController struct {
    20  	kClient k8s.Client
    21  
    22  	activeForwards map[k8s.PodID]portForwardEntry
    23  }
    24  
    25  func NewPortForwardController(kClient k8s.Client) *PortForwardController {
    26  	return &PortForwardController{
    27  		kClient:        kClient,
    28  		activeForwards: make(map[k8s.PodID]portForwardEntry),
    29  	}
    30  }
    31  
    32  // Figure out the diff between what's in the data store and
    33  // what port-forwarding is currently active.
    34  func (m *PortForwardController) diff(ctx context.Context, st store.RStore) (toStart []portForwardEntry, toShutdown []portForwardEntry) {
    35  	state := st.RLockState()
    36  	defer st.RUnlockState()
    37  
    38  	statePods := make(map[k8s.PodID]bool, len(state.ManifestTargets))
    39  
    40  	// Find all the port-forwards that need to be created.
    41  	for _, mt := range state.Targets() {
    42  		ms := mt.State
    43  		manifest := mt.Manifest
    44  		pod := ms.MostRecentPod()
    45  		podID := pod.PodID
    46  		if podID == "" {
    47  			continue
    48  		}
    49  
    50  		// Only do port-forwarding if the pod is running.
    51  		if pod.Phase != v1.PodRunning && !pod.Deleting {
    52  			continue
    53  		}
    54  
    55  		forwards := populatePortForwards(manifest, pod)
    56  		if len(forwards) == 0 {
    57  			continue
    58  		}
    59  
    60  		statePods[podID] = true
    61  
    62  		oldEntry, isActive := m.activeForwards[podID]
    63  		if isActive {
    64  			if cmp.Equal(oldEntry.forwards, forwards) {
    65  				continue
    66  			}
    67  			toShutdown = append(toShutdown, oldEntry)
    68  		}
    69  
    70  		ctx, cancel := context.WithCancel(ctx)
    71  		entry := portForwardEntry{
    72  			podID:     podID,
    73  			name:      ms.Name,
    74  			namespace: pod.Namespace,
    75  			forwards:  forwards,
    76  			ctx:       ctx,
    77  			cancel:    cancel,
    78  		}
    79  
    80  		toStart = append(toStart, entry)
    81  		m.activeForwards[podID] = entry
    82  	}
    83  
    84  	// Find all the port-forwards that aren't in the manifest anymore
    85  	// and need to be shutdown.
    86  	for key, value := range m.activeForwards {
    87  		_, inState := statePods[key]
    88  		if inState {
    89  			continue
    90  		}
    91  
    92  		toShutdown = append(toShutdown, value)
    93  		delete(m.activeForwards, key)
    94  	}
    95  
    96  	return toStart, toShutdown
    97  }
    98  
    99  func (m *PortForwardController) OnChange(ctx context.Context, st store.RStore) {
   100  	toStart, toShutdown := m.diff(ctx, st)
   101  	for _, entry := range toShutdown {
   102  		entry.cancel()
   103  	}
   104  
   105  	for _, entry := range toStart {
   106  		// Treat port-forwarding errors as part of the pod log
   107  		actionWriter := runtimelog.PodLogActionWriter{
   108  			Store:        st,
   109  			PodID:        entry.podID,
   110  			ManifestName: entry.name,
   111  		}
   112  		ctx := entry.ctx
   113  		ctx = logger.WithLogger(ctx, logger.NewLogger(logger.Get(ctx).Level(), actionWriter))
   114  
   115  		for _, forward := range entry.forwards {
   116  			entry := entry
   117  			forward := forward
   118  			go m.startPortForwardLoop(ctx, entry, forward)
   119  		}
   120  	}
   121  }
   122  
   123  func (m *PortForwardController) startPortForwardLoop(ctx context.Context, entry portForwardEntry, forward model.PortForward) {
   124  	originalBackoff := wait.Backoff{
   125  		Steps:    1000,
   126  		Duration: 50 * time.Millisecond,
   127  		Factor:   2.0,
   128  		Jitter:   0.1,
   129  		Cap:      15 * time.Second,
   130  	}
   131  	currentBackoff := originalBackoff
   132  
   133  	for {
   134  		start := time.Now()
   135  		err := m.onePortForward(ctx, entry, forward)
   136  		if ctx.Err() != nil {
   137  			// If the context was canceled, we're satisfied.
   138  			// Ignore any errors.
   139  			return
   140  		}
   141  
   142  		// Otherwise, repeat the loop, maybe logging the error
   143  		if err != nil {
   144  			logger.Get(ctx).Infof("Reconnecting... Error port-forwarding %s: %v", entry.name, err)
   145  		}
   146  
   147  		// If this failed in less than a second, then we should advance the backoff.
   148  		// Otherwise, reset the backoff.
   149  		if time.Since(start) < time.Second {
   150  			time.Sleep(currentBackoff.Step())
   151  		} else {
   152  			currentBackoff = originalBackoff
   153  		}
   154  	}
   155  }
   156  
   157  func (m *PortForwardController) onePortForward(ctx context.Context, entry portForwardEntry, forward model.PortForward) error {
   158  	ns := entry.namespace
   159  	podID := entry.podID
   160  
   161  	pf, err := m.kClient.CreatePortForwarder(ctx, ns, podID, forward.LocalPort, forward.ContainerPort, forward.Host)
   162  	if err != nil {
   163  		return err
   164  	}
   165  
   166  	err = pf.ForwardPorts()
   167  	if err != nil {
   168  		return err
   169  	}
   170  	return nil
   171  }
   172  
   173  var _ store.Subscriber = &PortForwardController{}
   174  
   175  type portForwardEntry struct {
   176  	name      model.ManifestName
   177  	namespace k8s.Namespace
   178  	podID     k8s.PodID
   179  	forwards  []model.PortForward
   180  	ctx       context.Context
   181  	cancel    func()
   182  }
   183  
   184  // Extract the port-forward specs from the manifest. If any of them
   185  // have ContainerPort = 0, populate them with the default port for the pod.
   186  // Quietly drop forwards that we can't populate.
   187  func populatePortForwards(m model.Manifest, pod store.Pod) []model.PortForward {
   188  	cPorts := pod.AllContainerPorts()
   189  	fwds := m.K8sTarget().PortForwards
   190  	forwards := make([]model.PortForward, 0, len(fwds))
   191  	for _, forward := range fwds {
   192  		if forward.ContainerPort == 0 {
   193  			if len(cPorts) == 0 {
   194  				continue
   195  			}
   196  
   197  			forward.ContainerPort = int(cPorts[0])
   198  			for _, cPort := range cPorts {
   199  				if int(forward.LocalPort) == int(cPort) {
   200  					forward.ContainerPort = int(cPort)
   201  					break
   202  				}
   203  			}
   204  		}
   205  		forwards = append(forwards, forward)
   206  	}
   207  	return forwards
   208  }
   209  
   210  func portForwardsAreValid(m model.Manifest, pod store.Pod) bool {
   211  	expectedFwds := m.K8sTarget().PortForwards
   212  	actualFwds := populatePortForwards(m, pod)
   213  	return len(actualFwds) == len(expectedFwds)
   214  }