github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/kubernetes/portforward/resource_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  	"sync"
    24  
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  
    27  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/constants"
    28  	kubernetesclient "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client"
    29  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    31  	schemautil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/util"
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    33  )
    34  
    35  // ResourceForwarder is responsible for forwarding user defined port forwarding resources and automatically forwarding
    36  // services deployed by skaffold.
    37  type ResourceForwarder struct {
    38  	output               io.Writer
    39  	entryManager         *EntryManager
    40  	label                string
    41  	kubeContext          string
    42  	userDefinedResources []*latest.PortForwardResource
    43  	services             bool
    44  }
    45  
    46  var (
    47  	// For testing
    48  	retrieveAvailablePort = util.GetAvailablePort
    49  	retrieveServices      = retrieveServiceResources
    50  )
    51  
    52  // NewServicesForwarder returns a struct that tracks and port-forwards services as they are created and modified
    53  func NewServicesForwarder(entryManager *EntryManager, kubeContext string, label string) *ResourceForwarder {
    54  	return &ResourceForwarder{
    55  		entryManager: entryManager,
    56  		label:        label,
    57  		services:     true,
    58  		kubeContext:  kubeContext,
    59  	}
    60  }
    61  
    62  // NewUserDefinedForwarder returns a struct that tracks and port-forwards services as they are created and modified
    63  func NewUserDefinedForwarder(entryManager *EntryManager, kubeContext string, userDefinedResources []*latest.PortForwardResource) *ResourceForwarder {
    64  	return &ResourceForwarder{
    65  		entryManager:         entryManager,
    66  		kubeContext:          kubeContext,
    67  		userDefinedResources: userDefinedResources,
    68  	}
    69  }
    70  
    71  // Start gets a list of services deployed by skaffold as []latest.PortForwardResource and
    72  // forwards them.
    73  func (p *ResourceForwarder) Start(ctx context.Context, out io.Writer, namespaces []string) error {
    74  	p.output = out
    75  	if len(namespaces) == 1 {
    76  		for _, pf := range p.userDefinedResources {
    77  			if err := applyWithTemplate(pf); err != nil {
    78  				return err
    79  			}
    80  			if pf.Namespace == "" {
    81  				pf.Namespace = namespaces[0]
    82  			}
    83  		}
    84  	} else {
    85  		var validResources []*latest.PortForwardResource
    86  		for _, pf := range p.userDefinedResources {
    87  			if pf.Namespace != "" {
    88  				if err := applyWithTemplate(pf); err != nil {
    89  					return err
    90  				}
    91  				validResources = append(validResources, pf)
    92  			} else {
    93  				log.Entry(ctx).Warnf("Skipping the port forwarding resource %s/%s because namespace is not specified", pf.Type, pf.Name)
    94  			}
    95  		}
    96  		p.userDefinedResources = validResources
    97  	}
    98  
    99  	var serviceResources []*latest.PortForwardResource
   100  	if p.services {
   101  		found, err := retrieveServices(ctx, p.label, namespaces, p.kubeContext)
   102  		if err != nil {
   103  			return fmt.Errorf("retrieving services for automatic port forwarding: %w", err)
   104  		}
   105  		serviceResources = found
   106  	}
   107  	p.portForwardResources(ctx, append(p.userDefinedResources, serviceResources...))
   108  	return nil
   109  }
   110  
   111  func applyWithTemplate(resource *latest.PortForwardResource) error {
   112  	if resource.Namespace != "" {
   113  		namespace, err := util.ExpandEnvTemplateOrFail(resource.Namespace, nil)
   114  		if err != nil {
   115  			return fmt.Errorf("cannot parse the namespace template on user defined port forwarder: %w", err)
   116  		}
   117  		resource.Namespace = namespace
   118  	}
   119  	name, err := util.ExpandEnvTemplateOrFail(resource.Name, nil)
   120  	if err != nil {
   121  		return fmt.Errorf("cannot parse the name template on user defined port forwarder: %w", err)
   122  	}
   123  	resource.Name = name
   124  	return nil
   125  }
   126  
   127  func (p *ResourceForwarder) Stop() {
   128  	p.entryManager.Stop()
   129  }
   130  
   131  // Port forward each resource individually in a goroutine
   132  func (p *ResourceForwarder) portForwardResources(ctx context.Context, resources []*latest.PortForwardResource) {
   133  	var wg sync.WaitGroup
   134  	for _, r := range resources {
   135  		wg.Add(1)
   136  		go func(r latest.PortForwardResource) {
   137  			defer wg.Done()
   138  			p.portForwardResource(ctx, r)
   139  		}(*r)
   140  	}
   141  	wg.Wait()
   142  }
   143  
   144  func (p *ResourceForwarder) portForwardResource(ctx context.Context, resource latest.PortForwardResource) {
   145  	// Get port forward entry for this resource
   146  	entry := p.getCurrentEntry(resource)
   147  	// Forward the entry
   148  	p.entryManager.forwardPortForwardEntry(ctx, p.output, entry)
   149  }
   150  
   151  func (p *ResourceForwarder) getCurrentEntry(resource latest.PortForwardResource) *portForwardEntry {
   152  	// determine if we have seen this before
   153  	entry := newPortForwardEntry(0, resource, "", "", "", "", 0, false)
   154  
   155  	// If we have, return the current entry
   156  	oe, ok := p.entryManager.forwardedResources.Load(entry.key())
   157  	if ok {
   158  		oldEntry := oe.(*portForwardEntry)
   159  		entry.localPort = oldEntry.localPort
   160  		return entry
   161  	}
   162  
   163  	// Try to request matching local port *providing* that it is not a system port.
   164  	// https://github.com/GoogleContainerTools/skaffold/pull/5554#issuecomment-803270340
   165  	requestPort := resource.LocalPort
   166  	if requestPort == 0 && resource.Port.IntVal >= 1024 {
   167  		requestPort = resource.Port.IntVal
   168  	}
   169  	entry.localPort = retrieveAvailablePort(resource.Address, requestPort, &p.entryManager.forwardedPorts)
   170  	return entry
   171  }
   172  
   173  // retrieveServiceResources retrieves all services in the cluster matching the given label
   174  // as a list of PortForwardResources
   175  func retrieveServiceResources(ctx context.Context, label string, namespaces []string, kubeContext string) ([]*latest.PortForwardResource, error) {
   176  	client, err := kubernetesclient.Client(kubeContext)
   177  	if err != nil {
   178  		return nil, fmt.Errorf("getting Kubernetes client: %w", err)
   179  	}
   180  
   181  	var resources []*latest.PortForwardResource
   182  	for _, ns := range namespaces {
   183  		services, err := client.CoreV1().Services(ns).List(ctx, metav1.ListOptions{
   184  			LabelSelector: label,
   185  		})
   186  		if err != nil {
   187  			return nil, fmt.Errorf("selecting services by label %q: %w", label, err)
   188  		}
   189  		for _, s := range services.Items {
   190  			for _, p := range s.Spec.Ports {
   191  				resources = append(resources, &latest.PortForwardResource{
   192  					Type:      constants.Service,
   193  					Name:      s.Name,
   194  					Namespace: s.Namespace,
   195  					Port:      schemautil.FromInt(int(p.Port)),
   196  					Address:   constants.DefaultPortForwardAddress,
   197  				})
   198  			}
   199  		}
   200  	}
   201  	return resources, nil
   202  }