github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/kubernetes/portforward/kubectl_forwarder.go (about) 1 /* 2 Copyright 2019 The Skaffold Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package portforward 18 19 import ( 20 "bufio" 21 "bytes" 22 "context" 23 "fmt" 24 "io" 25 "os" 26 "sort" 27 "strings" 28 "sync/atomic" 29 "time" 30 31 corev1 "k8s.io/api/core/v1" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/labels" 34 "k8s.io/apimachinery/pkg/util/intstr" 35 36 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubectl" 37 kubernetesclient "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client" 38 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output" 39 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log" 40 schemautil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/util" 41 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util" 42 ) 43 44 type EntryForwarder interface { 45 Start(io.Writer) 46 Forward(parentCtx context.Context, pfe *portForwardEntry) error 47 Terminate(p *portForwardEntry) 48 } 49 50 type KubectlForwarder struct { 51 started int32 52 out io.Writer 53 kubectl *kubectl.CLI 54 } 55 56 // NewKubectlForwarder returns a new KubectlForwarder 57 func NewKubectlForwarder(cli *kubectl.CLI) *KubectlForwarder { 58 return &KubectlForwarder{ 59 kubectl: cli, 60 } 61 } 62 63 // For testing 64 var ( 65 isPortFree = util.IsPortFree 66 findNewestPodForSvc = findNewestPodForService 67 deferFunc = func() {} 68 waitPortNotFree = 5 * time.Second 69 waitErrorLogs = 1 * time.Second 70 ) 71 72 func (k *KubectlForwarder) Start(out io.Writer) { 73 atomic.StoreInt32(&k.started, 1) 74 k.out = out 75 } 76 77 // Forward port-forwards a pod using kubectl port-forward in the background. 78 // It kills the command on errors in the kubectl port-forward log 79 // It restarts the command if it was not cancelled by skaffold 80 // It retries in case the port is taken 81 func (k *KubectlForwarder) Forward(parentCtx context.Context, pfe *portForwardEntry) error { 82 errChan := make(chan error, 1) 83 go k.forward(parentCtx, pfe, errChan) 84 l := log.Entry(parentCtx) 85 resourceName := "" 86 if pfe != nil { 87 resourceName = pfe.resource.Name 88 } 89 l.Tracef("KubectlForwarder.Forward(%s): waiting on errChan", resourceName) 90 select { 91 case <-parentCtx.Done(): 92 l.Tracef("KubectlForwarder.Forward(%s): parentCtx canceled, returning nil error", resourceName) 93 return nil 94 case err := <-errChan: 95 l.Tracef("KubectlForwarder.Forward(%s): got error on errChan, returning: %+v", resourceName, err) 96 return err 97 } 98 } 99 100 func (k *KubectlForwarder) forward(ctx context.Context, pfe *portForwardEntry, errChan chan error) { 101 if atomic.LoadInt32(&k.started) == 0 { 102 errChan <- fmt.Errorf("Forward() called before kubectl forwarder was started") 103 return 104 } 105 var notifiedUser bool 106 defer deferFunc() 107 108 for { 109 pfe.terminationLock.Lock() 110 if pfe.terminated { 111 log.Entry(ctx).Debugf("port forwarding %v was cancelled...", pfe) 112 pfe.terminationLock.Unlock() 113 errChan <- nil 114 return 115 } 116 pfe.terminationLock.Unlock() 117 118 if !isPortFree(util.Loopback, pfe.localPort) { 119 // Assuming that Skaffold brokered ports don't overlap, this has to be an external process that started 120 // since the dev loop kicked off. We are notifying the user in the hope that they can fix it 121 output.Red.Fprintf(k.out, "failed to port forward %v, port %d is taken, retrying...\n", pfe, pfe.localPort) 122 notifiedUser = true 123 time.Sleep(waitPortNotFree) 124 continue 125 } 126 127 if notifiedUser { 128 output.Green.Fprintf(k.out, "port forwarding %v recovered on port %d\n", pfe, pfe.localPort) 129 notifiedUser = false 130 } 131 132 ctx, cancel := context.WithCancel(ctx) 133 pfe.cancel = cancel 134 135 args := portForwardArgs(ctx, k.kubectl.KubeContext, pfe) 136 var buf bytes.Buffer 137 cmd := k.kubectl.CommandWithStrictCancellation(ctx, "port-forward", args...) 138 cmd.Stdout = &buf 139 cmd.Stderr = &buf 140 141 log.Entry(ctx).Debugf("Running command: %s", cmd.Args) 142 if err := cmd.Start(); err != nil { 143 if ctx.Err() == context.Canceled { 144 log.Entry(ctx).Debugf("couldn't start %v due to context cancellation", pfe) 145 return 146 } 147 // Retry on exit at Start() 148 log.Entry(ctx).Debugf("error starting port forwarding %v: %s, output: %s", pfe, err, buf.String()) 149 time.Sleep(500 * time.Millisecond) 150 continue 151 } 152 153 // Kill kubectl on port forwarding error logs 154 go k.monitorLogs(ctx, &buf, cmd, pfe, errChan) 155 if err := cmd.Wait(); err != nil { 156 if ctx.Err() == context.Canceled { 157 log.Entry(ctx).Debugf("terminated %v due to context cancellation", pfe) 158 return 159 } 160 // To make sure that the log monitor gets cleared up 161 cancel() 162 163 s := buf.String() 164 log.Entry(ctx).Debugf("port forwarding %v got terminated: %s, output: %s", pfe, err, s) 165 if !strings.Contains(s, "address already in use") { 166 select { 167 case errChan <- fmt.Errorf("port forwarding %v got terminated: output: %s", pfe, s): 168 default: 169 } 170 } 171 time.Sleep(500 * time.Millisecond) 172 } 173 } 174 } 175 176 func portForwardArgs(ctx context.Context, kubeContext string, pfe *portForwardEntry) []string { 177 args := []string{"--pod-running-timeout", "1s", "--namespace", pfe.resource.Namespace} 178 179 _, disableServiceForwarding := os.LookupEnv("SKAFFOLD_DISABLE_SERVICE_FORWARDING") 180 switch { 181 case pfe.resource.Type == "service" && !disableServiceForwarding: 182 // Services need special handling: https://github.com/GoogleContainerTools/skaffold/issues/4522 183 podName, remotePort, err := findNewestPodForSvc(ctx, kubeContext, pfe.resource.Namespace, pfe.resource.Name, pfe.resource.Port) 184 if err == nil { 185 args = append(args, fmt.Sprintf("pod/%s", podName), fmt.Sprintf("%d:%d", pfe.localPort, remotePort)) 186 break 187 } 188 log.Entry(ctx).Warnf("could not map pods to service %s/%s/%s: %v", pfe.resource.Namespace, pfe.resource.Name, pfe.resource.Port.String(), err) 189 fallthrough // and let kubectl try to handle it 190 191 default: 192 args = append(args, fmt.Sprintf("%s/%s", pfe.resource.Type, pfe.resource.Name), fmt.Sprintf("%d:%s", pfe.localPort, pfe.resource.Port.String())) 193 } 194 195 if pfe.resource.Address != "" && pfe.resource.Address != util.Loopback { 196 args = append(args, []string{"--address", pfe.resource.Address}...) 197 } 198 return args 199 } 200 201 // Terminate terminates an existing kubectl port-forward command using SIGTERM 202 func (*KubectlForwarder) Terminate(p *portForwardEntry) { 203 log.Entry(context.TODO()).Debugf("Terminating port-forward %v", p) 204 205 p.terminationLock.Lock() 206 defer p.terminationLock.Unlock() 207 208 if p.cancel != nil { 209 p.cancel() 210 } 211 p.terminated = true 212 } 213 214 // Monitor monitors the logs for a kubectl port forward command 215 // If it sees an error, it calls back to the EntryManager to 216 // retry the entire port forward operation. 217 func (*KubectlForwarder) monitorLogs(ctx context.Context, logs io.Reader, cmd *kubectl.Cmd, p *portForwardEntry, err chan error) { 218 ticker := time.NewTicker(waitErrorLogs) 219 defer ticker.Stop() 220 221 r := bufio.NewReader(logs) 222 for { 223 select { 224 case <-ctx.Done(): 225 return 226 case <-ticker.C: 227 s, _ := r.ReadString('\n') 228 if s == "" { 229 continue 230 } 231 232 log.Entry(ctx).Tracef("[port-forward] %s", s) 233 234 if strings.Contains(s, "error forwarding port") || 235 strings.Contains(s, "unable to forward") || 236 strings.Contains(s, "error upgrading connection") { 237 // kubectl is having an error. retry the command 238 log.Entry(ctx).Tracef("killing port forwarding %v", p) 239 if err := cmd.Terminate(); err != nil { 240 log.Entry(ctx).Tracef("failed to kill port forwarding %v, err: %s", p, err) 241 } 242 select { 243 case err <- fmt.Errorf("port forwarding %v got terminated: output: %s", p, s): 244 default: 245 } 246 return 247 } else if strings.Contains(s, "Forwarding from") { 248 select { 249 case err <- nil: 250 default: 251 } 252 } 253 } 254 } 255 } 256 257 // findNewestPodForService queries the cluster to find a pod that fulfills the given service, giving 258 // preference to pods that were most recently created. This is in contrast to the selection algorithm 259 // used by kubectl (see https://github.com/GoogleContainerTools/skaffold/issues/4522 for details). 260 func findNewestPodForService(ctx context.Context, kubeContext, ns, serviceName string, servicePort schemautil.IntOrString) (string, int, error) { 261 client, err := kubernetesclient.Client(kubeContext) 262 if err != nil { 263 return "", -1, fmt.Errorf("getting Kubernetes client: %w", err) 264 } 265 svc, err := client.CoreV1().Services(ns).Get(ctx, serviceName, metav1.GetOptions{}) 266 if err != nil { 267 return "", -1, fmt.Errorf("getting service %s/%s: %w", ns, serviceName, err) 268 } 269 svcPort, err := findServicePort(*svc, servicePort) 270 if err != nil { 271 return "", -1, err 272 } 273 274 // Look for pods with matching selectors and that are not terminated. 275 // We cannot use field selectors as they are only supported in 1.16 276 // https://github.com/flant/shell-operator/blob/8fa3c3b8cfeb1ddb37b070b7a871561fdffe788b/HOOKS.md#fieldselector 277 set := labels.Set(svc.Spec.Selector) 278 listOptions := metav1.ListOptions{ 279 LabelSelector: set.AsSelector().String(), 280 } 281 podsList, err := client.CoreV1().Pods(ns).List(ctx, listOptions) 282 if err != nil { 283 return "", -1, fmt.Errorf("listing pods: %w", err) 284 } 285 var pods []corev1.Pod 286 for _, pod := range podsList.Items { 287 if pod.Status.Phase == corev1.PodPending || pod.Status.Phase == corev1.PodRunning { 288 pods = append(pods, pod) 289 } 290 } 291 sort.Slice(pods, newestPodsFirst(pods)) 292 293 if log.IsTraceLevelEnabled() { 294 var names []string 295 for _, p := range pods { 296 names = append(names, fmt.Sprintf("(pod:%q phase:%v created:%v)", p.Name, p.Status.Phase, p.CreationTimestamp)) 297 } 298 log.Entry(ctx).Tracef("service %s/%s maps to %d pods: %v", serviceName, servicePort.String(), len(pods), names) 299 } 300 301 for _, p := range pods { 302 if targetPort := findTargetPort(svcPort, p); targetPort > 0 { 303 log.Entry(ctx).Debugf("Forwarding service %s/%s to pod %s/%d", serviceName, servicePort.String(), p.Name, targetPort) 304 return p.Name, targetPort, nil 305 } 306 } 307 308 return "", -1, fmt.Errorf("no pods match service %s/%s", serviceName, servicePort.String()) 309 } 310 311 // newestPodsFirst sorts pods by their creation time 312 func newestPodsFirst(pods []corev1.Pod) func(int, int) bool { 313 return func(i, j int) bool { 314 ti := pods[i].CreationTimestamp.Time 315 tj := pods[j].CreationTimestamp.Time 316 return ti.After(tj) 317 } 318 } 319 320 func findServicePort(svc corev1.Service, servicePort schemautil.IntOrString) (corev1.ServicePort, error) { 321 for _, s := range svc.Spec.Ports { 322 switch servicePort.Type { 323 case schemautil.Int: 324 if s.Port == int32(servicePort.IntVal) { 325 return s, nil 326 } 327 case schemautil.String: 328 if s.Name == servicePort.StrVal { 329 return s, nil 330 } 331 } 332 } 333 return corev1.ServicePort{}, fmt.Errorf("service %q does not expose port %s", svc.Name, servicePort.String()) 334 } 335 336 func findTargetPort(svcPort corev1.ServicePort, pod corev1.Pod) int { 337 if svcPort.TargetPort.Type == intstr.Int { 338 return svcPort.TargetPort.IntValue() 339 } 340 for _, c := range pod.Spec.Containers { 341 for _, p := range c.Ports { 342 if svcPort.TargetPort.StrVal == p.Name { 343 return int(p.ContainerPort) 344 } 345 } 346 } 347 return -1 348 }