Skip to content

Commit 5cf4103

Browse files
committed
ECS Agent dynamic host port assignment
1. Add GetHostPort() and update unit tests #3570 2. Upudate dockerPortMap() in task.go with dynamic host port range support part 1 #3584 3. Upudate dockerPortMap() in task.go with dynamic host port range support part 2 #3589 4. Validate the host port/host port range found by ECS Agent before returning it #3589 5. Refactor buildPortMapWithSCIngressConfig() in task.go #3600
1 parent 26e371e commit 5cf4103

File tree

5 files changed

+515
-122
lines changed

5 files changed

+515
-122
lines changed

agent/api/task/task.go

+132-35
Original file line numberDiff line numberDiff line change
@@ -2338,74 +2338,171 @@ func (task *Task) dockerLinks(container *apicontainer.Container, dockerContainer
23382338
}
23392339

23402340
var getHostPortRange = utils.GetHostPortRange
2341+
var getHostPort = utils.GetHostPort
23412342

2343+
// In buildPortMapWithSCIngressConfig, the dockerPortMap and the containerPortSet will be constructed
2344+
// for ingress listeners under two service connect bridge mode cases:
2345+
// (1) non-default bridge mode service connect experience: customers specify host ports for listeners in the ingress config.
2346+
// (2) default bridge mode service connect experience: customers do not specify host ports for listeners in the ingress config.
2347+
//
2348+
// Instead, ECS Agent finds host ports within the given dynamic host port range. An error will be returned for case (2) if
2349+
// ECS Agent cannot find an available host port within range.
2350+
func (task *Task) buildPortMapWithSCIngressConfig(dynamicHostPortRange string) (nat.PortMap, error) {
2351+
var err error
2352+
ingressDockerPortMap := nat.PortMap{}
2353+
ingressContainerPortSet := make(map[int]struct{})
2354+
protocolStr := "tcp"
2355+
scContainer := task.GetServiceConnectContainer()
2356+
for _, ic := range task.ServiceConnectConfig.IngressConfig {
2357+
listenerPortInt := int(ic.ListenerPort)
2358+
dockerPort := nat.Port(strconv.Itoa(listenerPortInt) + "/" + protocolStr)
2359+
hostPortStr := ""
2360+
if ic.HostPort != nil {
2361+
// For non-default bridge mode service connect experience, a host port is specified by customers
2362+
// Note that service connect ingress config has been validated in service_connect_validator.go,
2363+
// where host ports will be validated to ensure user-definied ports are within a valid port range (1 to 65535)
2364+
// and do not have port collisions.
2365+
hostPortStr = strconv.Itoa(int(*ic.HostPort))
2366+
} else {
2367+
// For default bridge mode service connect experience, customers do not specify a host port
2368+
// thus the host port will be assigned by ECS Agent.
2369+
// ECS Agent will find an available host port within the given dynamic host port range,
2370+
// or return an error if no host port is available within the range.
2371+
hostPortStr, err = getHostPort(protocolStr, dynamicHostPortRange)
2372+
if err != nil {
2373+
return nil, err
2374+
}
2375+
}
2376+
2377+
ingressDockerPortMap[dockerPort] = append(ingressDockerPortMap[dockerPort], nat.PortBinding{HostPort: hostPortStr})
2378+
// Append non-range, singular container port to the ingressContainerPortSet
2379+
ingressContainerPortSet[listenerPortInt] = struct{}{}
2380+
// Set taskContainer.ContainerPortSet to be used during network binding creation
2381+
scContainer.SetContainerPortSet(ingressContainerPortSet)
2382+
}
2383+
return ingressDockerPortMap, err
2384+
}
2385+
2386+
// dockerPortMap creates a port binding map for
2387+
// (1) Ingress listeners for the service connect AppNet container in the service connect bridge network mode task.
2388+
// (2) Port mapping configured by customers in the task definition.
2389+
//
2390+
// For service connect bridge mode task, we will create port bindings for customers' application containers
2391+
// and service connect AppNet container, and let them to be published by the associated pause containers.
2392+
// (a) For default bridge service connect experience, ECS Agent will assign a host port within the
2393+
// default/user-specified dynamic host port range for the ingress listener. If no available host port can be
2394+
// found by ECS Agent, an error will be returned.
2395+
// (b) For non-default bridge service connect experience, ECS Agent will use the user-defined host port for the ingress listener.
2396+
//
2397+
// For non-service connect bridge network mode task, ECS Agent will assign a host port or a host port range
2398+
// within the default/user-specified dynamic host port range. If no available host port or host port range can be
2399+
// found by ECS Agent, an error will be returned.
2400+
//
2401+
// Note that
2402+
// (a) ECS Agent will not assign a new host port within the dynamic host port range for awsvpc network mode task
2403+
// (b) ECS Agent will not assign a new host port within the dynamic host port range if the user-specified host port exists
23422404
func (task *Task) dockerPortMap(container *apicontainer.Container, dynamicHostPortRange string) (nat.PortMap, error) {
2405+
hostPortStr := ""
23432406
dockerPortMap := nat.PortMap{}
2344-
scContainer := task.GetServiceConnectContainer()
23452407
containerToCheck := container
23462408
containerPortSet := make(map[int]struct{})
23472409
containerPortRangeMap := make(map[string]string)
2410+
2411+
// For service connect bridge network mode task, we will create port bindings for task containers,
2412+
// including both application containers and service connect AppNet container, and let them to be published
2413+
// by the associated pause containers.
23482414
if task.IsServiceConnectEnabled() && task.IsNetworkModeBridge() {
23492415
if container.Type == apicontainer.ContainerCNIPause {
2350-
// we will create bindings for task containers (including both customer containers and SC Appnet container)
2351-
// and let them be published by the associated pause container.
2352-
// Note - for SC bridge mode we do not allow customer to specify a host port for their containers. Additionally,
2353-
// When an ephemeral host port is assigned, Appnet will NOT proxy traffic to that port
2416+
// Find the task container associated with this particular pause container
23542417
taskContainer, err := task.getBridgeModeTaskContainerForPauseContainer(container)
23552418
if err != nil {
23562419
return nil, err
23572420
}
2421+
2422+
scContainer := task.GetServiceConnectContainer()
23582423
if taskContainer == scContainer {
2359-
// create bindings for all ingress listener ports
2360-
// no need to create binding for egress listener port as it won't be access from host level or from outside
2361-
for _, ic := range task.ServiceConnectConfig.IngressConfig {
2362-
listenerPortInt := int(ic.ListenerPort)
2363-
dockerPort := nat.Port(strconv.Itoa(listenerPortInt)) + "/tcp"
2364-
hostPort := 0 // default bridge-mode SC experience - host port will be an ephemeral port assigned by docker
2365-
if ic.HostPort != nil { // non-default bridge-mode SC experience - host port specified by customer
2366-
hostPort = int(*ic.HostPort)
2367-
}
2368-
dockerPortMap[dockerPort] = append(dockerPortMap[dockerPort], nat.PortBinding{HostPort: strconv.Itoa(hostPort)})
2369-
// append non-range, singular container port to the containerPortSet
2370-
containerPortSet[listenerPortInt] = struct{}{}
2371-
// set taskContainer.ContainerPortSet to be used during network binding creation
2372-
taskContainer.SetContainerPortSet(containerPortSet)
2424+
// If the associated task container to this pause container is the service connect AppNet container,
2425+
// create port binding(s) for ingress listener ports based on its ingress config.
2426+
// Note that there is no need to do this for egress listener ports as they won't be accessed
2427+
// from host level or from outside.
2428+
dockerPortMap, err := task.buildPortMapWithSCIngressConfig(dynamicHostPortRange)
2429+
if err != nil {
2430+
logger.Error("Failed to build a port map with service connect ingress config", logger.Fields{
2431+
field.TaskID: task.GetID(),
2432+
field.Container: taskContainer.Name,
2433+
"dynamicHostPortRange": dynamicHostPortRange,
2434+
field.Error: err,
2435+
})
2436+
return nil, err
23732437
}
23742438
return dockerPortMap, nil
23752439
}
2440+
// If the associated task container to this pause container is NOT the service connect AppNet container,
2441+
// we will continue to update the dockerPortMap for the pause container using the port bindings
2442+
// configured for the application container since port bindings will be published by the pasue container.
23762443
containerToCheck = taskContainer
23772444
} else {
2378-
// If container is neither SC container nor pause container, it's a regular task container. Its port bindings(s)
2379-
// are published by the associated pause container, and we leave the map empty here (docker would actually complain
2380-
// otherwise).
2445+
// If the container is not a pause container, then it is a regular customers' application container
2446+
// or a service connect AppNet container. We will leave the map empty and return it as its port bindings(s)
2447+
// are published by the associated pause container.
23812448
return dockerPortMap, nil
23822449
}
23832450
}
23842451

2452+
// For each port binding config, either one of containerPort or containerPortRange is set.
2453+
// (1) containerPort is the port number on the container that's bound to the user-specified host port or the
2454+
// host port assigned by ECS Agent.
2455+
// (2) containerPortRange is the port number range on the container that's bound to the mapped host port range
2456+
// found by ECS Agent.
2457+
var err error
23852458
for _, portBinding := range containerToCheck.Ports {
2386-
// for each port binding config, either one of containerPort or containerPortRange is set
23872459
if portBinding.ContainerPort != 0 {
23882460
containerPort := int(portBinding.ContainerPort)
2461+
protocolStr := portBinding.Protocol.String()
2462+
dockerPort := nat.Port(strconv.Itoa(containerPort) + "/" + protocolStr)
2463+
2464+
if portBinding.HostPort != 0 {
2465+
// An user-specified host port exists.
2466+
// Note that the host port value has been validated by ECS front end service;
2467+
// thus only an valid host port value will be streamed down to ECS Agent.
2468+
hostPortStr = strconv.Itoa(int(portBinding.HostPort))
2469+
} else {
2470+
// If there is no user-specified host port, ECS Agent will find an available host port
2471+
// within the given dynamic host port range. And if no host port is available within the range,
2472+
// an error will be returned.
2473+
logger.Debug("No user-specified host port, ECS Agent will find an available host port within the given dynamic host port range", logger.Fields{
2474+
field.Container: containerToCheck.Name,
2475+
"dynamicHostPortRange": dynamicHostPortRange,
2476+
})
2477+
hostPortStr, err = getHostPort(protocolStr, dynamicHostPortRange)
2478+
if err != nil {
2479+
logger.Error("Unable to find a host port for container within the given dynamic host port range", logger.Fields{
2480+
field.TaskID: task.GetID(),
2481+
field.Container: container.Name,
2482+
"dynamicHostPortRange": dynamicHostPortRange,
2483+
field.Error: err,
2484+
})
2485+
return nil, err
2486+
}
2487+
}
2488+
dockerPortMap[dockerPort] = append(dockerPortMap[dockerPort], nat.PortBinding{HostPort: hostPortStr})
23892489

2390-
dockerPort := nat.Port(strconv.Itoa(containerPort) + "/" + portBinding.Protocol.String())
2391-
dockerPortMap[dockerPort] = append(dockerPortMap[dockerPort], nat.PortBinding{HostPort: strconv.Itoa(int(portBinding.HostPort))})
2392-
2393-
// append non-range, singular container port to the containerPortSet
2490+
// For the containerPort case, append a non-range, singular container port to the containerPortSet.
23942491
containerPortSet[containerPort] = struct{}{}
23952492
} else if portBinding.ContainerPortRange != "" {
23962493
containerToCheck.SetContainerHasPortRange(true)
23972494

23982495
containerPortRange := portBinding.ContainerPortRange
2399-
// nat.ParsePortRangeToInt validates a port range; if valid, it returns start and end ports as integers
2496+
// nat.ParsePortRangeToInt validates a port range; if valid, it returns start and end ports as integers.
24002497
startContainerPort, endContainerPort, err := nat.ParsePortRangeToInt(containerPortRange)
24012498
if err != nil {
24022499
return nil, err
24032500
}
24042501

24052502
numberOfPorts := endContainerPort - startContainerPort + 1
24062503
protocol := portBinding.Protocol.String()
2407-
// we will try to get a contiguous set of host ports from the ephemeral host port range.
2408-
// this is to ensure that docker maps host ports in a contiguous manner, and
2504+
// We will try to get a contiguous set of host ports from the ephemeral host port range.
2505+
// This is to ensure that docker maps host ports in a contiguous manner, and
24092506
// we are guaranteed to have the entire hostPortRange in a single network binding while sending this info to ECS;
24102507
// therefore, an error will be returned if we cannot find a contiguous set of host ports.
24112508
hostPortRange, err := getHostPortRange(numberOfPorts, protocol, dynamicHostPortRange)
@@ -2419,8 +2516,8 @@ func (task *Task) dockerPortMap(container *apicontainer.Container, dynamicHostPo
24192516
return nil, err
24202517
}
24212518

2422-
// append ranges to the dockerPortMap
2423-
// nat.ParsePortSpec returns a list of port mappings in a format that docker likes
2519+
// For the ContainerPortRange case, append ranges to the dockerPortMap.
2520+
// nat.ParsePortSpec returns a list of port mappings in a format that Docker likes.
24242521
mappings, err := nat.ParsePortSpec(hostPortRange + ":" + containerPortRange + "/" + protocol)
24252522
if err != nil {
24262523
return nil, err
@@ -2430,13 +2527,13 @@ func (task *Task) dockerPortMap(container *apicontainer.Container, dynamicHostPo
24302527
dockerPortMap[mapping.Port] = append(dockerPortMap[mapping.Port], mapping.Binding)
24312528
}
24322529

2433-
// append containerPortRange and associated hostPortRange to the containerPortRangeMap
2434-
// this will ensure that we consolidate range into 1 network binding while sending it to ECS
2530+
// For the ContainerPortRange case, append containerPortRange and associated hostPortRange to the containerPortRangeMap.
2531+
// This will ensure that we consolidate range into 1 network binding while sending it to ECS.
24352532
containerPortRangeMap[containerPortRange] = hostPortRange
24362533
}
24372534
}
24382535

2439-
// set Container.ContainerPortSet and Container.ContainerPortRangeMap to be used during network binding creation
2536+
// Set Container.ContainerPortSet and Container.ContainerPortRangeMap to be used during network binding creation.
24402537
containerToCheck.SetContainerPortSet(containerPortSet)
24412538
containerToCheck.SetContainerPortRangeMap(containerPortRangeMap)
24422539
return dockerPortMap, nil

0 commit comments

Comments
 (0)