github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/kubernetes/portforward/pod_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  	"context"
    21  	"fmt"
    22  	"io"
    23  	"strconv"
    24  
    25  	v1 "k8s.io/api/core/v1"
    26  	"k8s.io/apimachinery/pkg/watch"
    27  
    28  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
    29  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes"
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    33  	schemautil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/util"
    34  )
    35  
    36  var (
    37  	// For testing
    38  	newPodWatcher    = kubernetes.NewPodWatcher
    39  	topLevelOwnerKey = kubernetes.TopLevelOwnerKey
    40  )
    41  
    42  // WatchingPodForwarder is responsible for selecting pods satisfying a certain condition and port-forwarding the exposed
    43  // container ports within those pods. It also tracks and manages the port-forward connections.
    44  type WatchingPodForwarder struct {
    45  	output       io.Writer
    46  	entryManager *EntryManager
    47  	podWatcher   kubernetes.PodWatcher
    48  	events       chan kubernetes.PodEvent
    49  	kubeContext  string
    50  
    51  	// portSelector returns a possibly-filtered and possibly-generated set of ports for a pod.
    52  	containerPorts portSelector
    53  }
    54  
    55  // portSelector selects a set of ContainerPorts from a container in a pod.
    56  type portSelector func(*v1.Pod, v1.Container) []v1.ContainerPort
    57  
    58  // NewWatchingPodForwarder returns a struct that tracks and port-forwards pods as they are created and modified
    59  func NewWatchingPodForwarder(entryManager *EntryManager, kubeContext string, podSelector kubernetes.PodSelector, containerPorts portSelector) *WatchingPodForwarder {
    60  	return &WatchingPodForwarder{
    61  		entryManager:   entryManager,
    62  		podWatcher:     newPodWatcher(podSelector),
    63  		events:         make(chan kubernetes.PodEvent),
    64  		kubeContext:    kubeContext,
    65  		containerPorts: containerPorts,
    66  	}
    67  }
    68  
    69  func (p *WatchingPodForwarder) Start(ctx context.Context, out io.Writer, namespaces []string) error {
    70  	p.podWatcher.Register(p.events)
    71  	p.output = out
    72  	stopWatcher, err := p.podWatcher.Start(ctx, p.kubeContext, namespaces)
    73  	if err != nil {
    74  		return err
    75  	}
    76  
    77  	go func() {
    78  		defer stopWatcher()
    79  		l := log.Entry(ctx)
    80  		defer l.Tracef("podForwarder: cease waiting for pod events")
    81  		l.Tracef("podForwarder: waiting for pod events")
    82  		for {
    83  			select {
    84  			case <-ctx.Done():
    85  				l.Tracef("podForwarder: context canceled, ignoring")
    86  			case evt, ok := <-p.events:
    87  				if !ok {
    88  					l.Tracef("podForwarder: channel closed, returning")
    89  					return
    90  				}
    91  
    92  				// At this point, we know the event's type is "ADDED" or "MODIFIED".
    93  				// We must take both types into account as it is possible for the pod to have become ready for port-forwarding before we established the watch.
    94  				pod := evt.Pod
    95  				if evt.Type != watch.Deleted && pod.Status.Phase == v1.PodRunning && pod.DeletionTimestamp == nil {
    96  					if err := p.portForwardPod(ctx, pod); err != nil {
    97  						log.Entry(ctx).Warnf("port forwarding pod failed: %s", err)
    98  					}
    99  				}
   100  			}
   101  		}
   102  	}()
   103  
   104  	return nil
   105  }
   106  
   107  func (p *WatchingPodForwarder) Stop() {
   108  	p.entryManager.Stop()
   109  	p.podWatcher.Deregister(p.events)
   110  }
   111  
   112  func (p *WatchingPodForwarder) portForwardPod(ctx context.Context, pod *v1.Pod) error {
   113  	ownerReference := topLevelOwnerKey(ctx, pod, p.kubeContext, pod.Kind)
   114  	for _, c := range pod.Spec.Containers {
   115  		for _, port := range p.containerPorts(pod, c) {
   116  			// get current entry for this container
   117  			resource := latest.PortForwardResource{
   118  				Type:      constants.Pod,
   119  				Name:      pod.Name,
   120  				Namespace: pod.Namespace,
   121  				Port:      schemautil.FromInt(int(port.ContainerPort)),
   122  				Address:   constants.DefaultPortForwardAddress,
   123  			}
   124  
   125  			entry, err := p.podForwardingEntry(pod.ResourceVersion, c.Name, port.Name, ownerReference, resource)
   126  			if err != nil {
   127  				return fmt.Errorf("getting pod forwarding entry: %w", err)
   128  			}
   129  			if entry.resource.Port.IntVal != entry.localPort {
   130  				output.Yellow.Fprintf(p.output, "Forwarding container %s/%s to local port %d.\n", pod.Name, c.Name, entry.localPort)
   131  			}
   132  			if pe, ok := p.entryManager.forwardedResources.Load(entry.key()); ok {
   133  				prevEntry := pe.(*portForwardEntry)
   134  				// Check if this is a new generation of pod
   135  				if entry.resourceVersion > prevEntry.resourceVersion {
   136  					p.entryManager.Terminate(prevEntry)
   137  				}
   138  			}
   139  			p.entryManager.forwardPortForwardEntry(ctx, p.output, entry)
   140  		}
   141  	}
   142  	return nil
   143  }
   144  
   145  func (p *WatchingPodForwarder) podForwardingEntry(resourceVersion, containerName, portName, ownerReference string, resource latest.PortForwardResource) (*portForwardEntry, error) {
   146  	rv, err := strconv.Atoi(resourceVersion)
   147  	if err != nil {
   148  		return nil, fmt.Errorf("converting resource version to integer: %w", err)
   149  	}
   150  	entry := newPortForwardEntry(rv, resource, resource.Name, containerName, portName, ownerReference, 0, true)
   151  
   152  	// If we have, return the current entry
   153  	oe, ok := p.entryManager.forwardedResources.Load(entry.key())
   154  
   155  	if ok {
   156  		oldEntry := oe.(*portForwardEntry)
   157  		entry.localPort = oldEntry.localPort
   158  		return entry, nil
   159  	}
   160  
   161  	// retrieve an open port on the host
   162  	entry.localPort = retrieveAvailablePort(resource.Address, resource.Port.IntVal, &p.entryManager.forwardedPorts)
   163  
   164  	return entry, nil
   165  }