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 }