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  }