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 }