github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/deploy/docker/port.go (about)

     1  /*
     2  Copyright 2021 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 docker
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"strconv"
    24  	"strings"
    25  	"sync"
    26  
    27  	"github.com/docker/docker/api/types/container"
    28  	"github.com/docker/go-connections/nat"
    29  
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/event"
    31  	eventV2 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/event/v2"
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
    33  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
    34  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    35  	schemautil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/util"
    36  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    37  )
    38  
    39  var GetAvailablePort = util.GetAvailablePort // For testing
    40  
    41  type containerPortForwardEntry struct {
    42  	container       string
    43  	resourceName    string
    44  	resourceAddress string
    45  	localPort       int32
    46  	remotePort      schemautil.IntOrString
    47  }
    48  
    49  type PortManager struct {
    50  	containerPorts map[string][]int // maps containers to the ports they have allocated
    51  	portSet        util.PortSet
    52  	entries        []containerPortForwardEntry // reference shared with DockerForwarder so output is issued in the correct phase of the dev loop
    53  	lock           sync.Mutex
    54  }
    55  
    56  func NewPortManager() *PortManager {
    57  	return &PortManager{
    58  		containerPorts: make(map[string][]int),
    59  		portSet:        util.PortSet{},
    60  	}
    61  }
    62  
    63  func (pm *PortManager) Start(_ context.Context, out io.Writer) error {
    64  	pm.lock.Lock()
    65  	defer pm.lock.Unlock()
    66  	pm.containerPortForwardEvents(out)
    67  	return nil
    68  }
    69  
    70  func (pm *PortManager) Stop() {
    71  	pm.lock.Lock()
    72  	defer pm.lock.Unlock()
    73  	pm.entries = nil
    74  }
    75  
    76  /*
    77  	allocatePorts converts PortForwardResources into docker.PortSet objects, and combines them with
    78  	pre-configured debug bindings into one docker.PortMap. The debug bindings will have their
    79  	requested host ports validated against the port tracker, and modified if a port collision is found.
    80  
    81  	These ports are added to the provided container configuration's port set, and the bindings
    82  	are returned to be passed to ContainerCreate on Deploy to expose container ports on the host.
    83  */
    84  
    85  func (pm *PortManager) allocatePorts(containerName string, pf []*latest.PortForwardResource, cfg *container.Config, debugBindings nat.PortMap) (nat.PortMap, error) {
    86  	pm.lock.Lock()
    87  	defer pm.lock.Unlock()
    88  	m := make(nat.PortMap)
    89  	var entries []containerPortForwardEntry
    90  	if cfg.ExposedPorts == nil {
    91  		cfg.ExposedPorts = nat.PortSet{}
    92  	}
    93  	var ports []int
    94  	for _, p := range pf {
    95  		if strings.ToLower(string(p.Type)) != "container" {
    96  			log.Entry(context.TODO()).Debugf("skipping non-container port forward resource in Docker deploy: %s\n", p.Name)
    97  			continue
    98  		}
    99  		localPort := GetAvailablePort(p.Address, p.LocalPort, &pm.portSet)
   100  		ports = append(ports, localPort)
   101  		port, err := nat.NewPort("tcp", p.Port.String())
   102  		if err != nil {
   103  			return nil, err
   104  		}
   105  		cfg.ExposedPorts[port] = struct{}{}
   106  		m[port] = []nat.PortBinding{
   107  			{HostIP: p.Address, HostPort: fmt.Sprintf("%d", localPort)},
   108  		}
   109  		entries = append(entries, containerPortForwardEntry{
   110  			container:       containerName,
   111  			resourceName:    p.Name,
   112  			resourceAddress: p.Address,
   113  			localPort:       int32(localPort),
   114  			remotePort:      p.Port,
   115  		})
   116  	}
   117  
   118  	// we can't modify the existing debug bindings in place, since they are not passed by reference.
   119  	// instead, copy each binding and modify the copy, then insert into a new map and return that.
   120  	for port, bindings := range debugBindings {
   121  		modifiedBindings := make([]nat.PortBinding, len(bindings))
   122  		for i, b := range bindings {
   123  			newBinding := nat.PortBinding{HostIP: b.HostIP, HostPort: b.HostPort}
   124  			hostPort, err := strconv.Atoi(newBinding.HostPort)
   125  			if err != nil {
   126  				return nil, err
   127  			}
   128  			localPort := GetAvailablePort(newBinding.HostIP, hostPort, &pm.portSet)
   129  			if localPort != hostPort {
   130  				newBinding.HostPort = strconv.Itoa(localPort)
   131  			}
   132  			ports = append(ports, localPort)
   133  			cfg.ExposedPorts[port] = struct{}{}
   134  			entries = append(entries, containerPortForwardEntry{
   135  				container:       containerName,
   136  				resourceAddress: newBinding.HostIP,
   137  				localPort:       int32(localPort),
   138  				remotePort: schemautil.IntOrString{
   139  					Type:   schemautil.Int,
   140  					IntVal: port.Int(),
   141  				},
   142  			})
   143  			modifiedBindings[i] = newBinding
   144  		}
   145  		m[port] = modifiedBindings
   146  	}
   147  	pm.containerPorts[containerName] = ports
   148  
   149  	// register entries on the port manager, to be issued by the event handler later
   150  	pm.entries = append(pm.entries, entries...)
   151  	return m, nil
   152  }
   153  
   154  func (pm *PortManager) relinquishPorts(containerName string) {
   155  	pm.lock.Lock()
   156  	defer pm.lock.Unlock()
   157  	ports := pm.containerPorts[containerName]
   158  	for _, port := range ports {
   159  		pm.portSet.Delete(port)
   160  	}
   161  	pm.containerPorts[containerName] = nil
   162  }
   163  
   164  func (pm *PortManager) containerPortForwardEvents(out io.Writer) {
   165  	for _, entry := range pm.entries {
   166  		event.PortForwarded(
   167  			entry.localPort,
   168  			entry.remotePort,
   169  			"",              // no pod name
   170  			entry.container, // container name
   171  			"",              // no namespace
   172  			"",              // no port name
   173  			"container",
   174  			entry.resourceName,
   175  			entry.resourceAddress,
   176  		)
   177  
   178  		eventV2.PortForwarded(
   179  			entry.localPort,
   180  			entry.remotePort,
   181  			"",              // no pod name
   182  			entry.container, // container name
   183  			"",              // no namespace
   184  			"",              // no port name
   185  			"container",
   186  			entry.resourceName,
   187  			entry.resourceAddress,
   188  		)
   189  
   190  		output.Green.Fprintln(out,
   191  			fmt.Sprintf("[%s] Forwarding container port %s -> local port http://%s:%d",
   192  				entry.container,
   193  				entry.remotePort.String(),
   194  				entry.resourceAddress,
   195  				entry.localPort,
   196  			))
   197  	}
   198  }