Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DHPA] Upudate dockerPortMap() in task.go with dynamic host port range support - part 2+ #3589

Merged
merged 2 commits into from
Feb 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 89 additions & 32 deletions agent/api/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -2340,64 +2340,119 @@ func (task *Task) dockerLinks(container *apicontainer.Container, dockerContainer
var getHostPortRange = utils.GetHostPortRange
var getHostPort = utils.GetHostPort

// In buildPortMapWithSCIngressConfig, the dockerPortMap and the containerPortSet will be constructed
// for ingress listeners under two service connect bridge mode cases:
// (1) non-default bridge mode service connect experience: customers specify host ports for listeners in the ingress config.
// (2) default bridge mode service connect experience: customers do not specify host ports for listeners in the ingress config.
//
// Instead, ECS Agent finds host ports within the given dynamic host port range. An error will be returned for case (2) if
// ECS Agent cannot find an available host port within range.
func (task *Task) buildPortMapWithSCIngressConfig(dynamicHostPortRange string) (nat.PortMap, map[int]struct{}, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function buildPortMapWithSCIngressConfig() has been reviewed in #3585.

var err error
ingressDockerPortMap := nat.PortMap{}
ingressContainerPortSet := make(map[int]struct{})
protocolStr := "tcp"
for _, ic := range task.ServiceConnectConfig.IngressConfig {
listenerPortInt := int(ic.ListenerPort)
dockerPort := nat.Port(strconv.Itoa(listenerPortInt) + "/" + protocolStr)
hostPortStr := ""
if ic.HostPort != nil {
// For non-default bridge mode service connect experience, a host port is specified by customers
// Note that service connect ingress config has been validated in service_connect_validator.go,
// where host ports will be validated to ensure user-definied ports are within a valid port range (1 to 65535)
// and do not have port collisions.
hostPortStr = strconv.Itoa(int(*ic.HostPort))
} else {
// For default bridge mode service connect experience, customers do not specify a host port
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments from line 2366-2369 are newly added in this PR.

// thus the host port will be assigned by ECS Agent.
// ECS Agent will find an available host port within the given dynamic host port range,
// or return an error if no host port is available within the range.
hostPortStr, err = getHostPort(protocolStr, dynamicHostPortRange)
if err != nil {
return nil, nil, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit(non-blocking): maybe we can add a logging statement here saying no host port was available.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't have to add a logging statement here as the error returned from this function will be surfaced on line 2426 : )

}
}

ingressDockerPortMap[dockerPort] = append(ingressDockerPortMap[dockerPort], nat.PortBinding{HostPort: hostPortStr})
// Append non-range, singular container port to the ingressContainerPortSet
ingressContainerPortSet[listenerPortInt] = struct{}{}
}
return ingressDockerPortMap, ingressContainerPortSet, err
}

// dockerPortMap creates a port binding map for
// (1) Ingress listeners for the service connect AppNet container in the service connect enabled bridge network mode task.
// (2) Port mapping definied by customers in the task definition.
// (1) Ingress listeners for the service connect AppNet container in the service connect bridge network mode task.
// (2) Port mapping configured by customers in the task definition.
//
// For service connect bridge mode task, we will create port bindings for customers' application containers
// and service connect AppNet container, and let them to be published by the associated pause containers.
// (a) For default bridge service connect experience, ECS Agent will assign a host port within the
// default/user-specified dynamic host port range for the ingress listener. If no available host port can be
// found by ECS Agent, an error will be returned.
// (b) For non-default bridge service connect experience, ECS Agent will use the user-defined host port for the ingress listener.
//
// For non-service connect bridge network mode task, ECS Agent will assign a host port or a host port range
// within the default/user-specified dynamic host port range. If no available host port or host port range can be
// found by ECS Agent, an error will be returned.
//
// Note that,
// Note that
// (a) ECS Agent will not assign a new host port within the dynamic host port range for awsvpc network mode task
// (b) ECS Agent will not assign a new host port within the dynamic host port range if the user-specified host port exists
func (task *Task) dockerPortMap(container *apicontainer.Container, dynamicHostPortRange string) (nat.PortMap, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function dockerPortMap() has been reviewed in #3585.

hostPortStr := ""
dockerPortMap := nat.PortMap{}
scContainer := task.GetServiceConnectContainer()
containerToCheck := container
containerPortSet := make(map[int]struct{})
containerPortRangeMap := make(map[string]string)

// For service connect bridge network mode task, we will create port bindings for task containers,
// including both application containers and service connect AppNet container, and let them to be published
// by the associated pause containers.
if task.IsServiceConnectEnabled() && task.IsNetworkModeBridge() {
if container.Type == apicontainer.ContainerCNIPause {
// we will create bindings for task containers (including both customer containers and SC Appnet container)
// and let them be published by the associated pause container.
// Note - for SC bridge mode we do not allow customer to specify a host port for their containers. Additionally,
// When an ephemeral host port is assigned, Appnet will NOT proxy traffic to that port
// Find the task container associated with this particular pause container
taskContainer, err := task.getBridgeModeTaskContainerForPauseContainer(container)
if err != nil {
return nil, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit(non-blocking): can also maybe add a logging statement here

Copy link
Contributor Author

@chienhanlin chienhanlin Feb 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! I saw that getBridgeModeTaskContainerForPauseContainer() has been used in dockerExposedPorts() and PopulateServiceConnectContainerMappingEnvVar() as well. These functions are related to createContainer, thus the error returned from getBridgeModeTaskContainerForPauseContainer() will eventually be logged as dockerapi.DockerContainerMetadata{Error: XXX} in ecs-agent.log and also be reported back to ECS control plane.

}

scContainer := task.GetServiceConnectContainer()
if taskContainer == scContainer {
// create bindings for all ingress listener ports
// no need to create binding for egress listener port as it won't be access from host level or from outside
for _, ic := range task.ServiceConnectConfig.IngressConfig {
listenerPortInt := int(ic.ListenerPort)
dockerPort := nat.Port(strconv.Itoa(listenerPortInt)) + "/tcp"
hostPort := 0 // default bridge-mode SC experience - host port will be an ephemeral port assigned by docker
if ic.HostPort != nil { // non-default bridge-mode SC experience - host port specified by customer
hostPort = int(*ic.HostPort)
}
dockerPortMap[dockerPort] = append(dockerPortMap[dockerPort], nat.PortBinding{HostPort: strconv.Itoa(hostPort)})
// append non-range, singular container port to the containerPortSet
containerPortSet[listenerPortInt] = struct{}{}
// set taskContainer.ContainerPortSet to be used during network binding creation
taskContainer.SetContainerPortSet(containerPortSet)
// If the associated task container to this pause container is the service connect AppNet container,
// create port binding(s) for ingress listener ports based on its ingress config.
// Note that there is no need to do this for egress listener ports as they won't be accessed
// from host level or from outside.
dockerPortMap, containerPortSet, err := task.buildPortMapWithSCIngressConfig(dynamicHostPortRange)
if err != nil {
logger.Error("Failed to build a port map with service connect ingress config", logger.Fields{
field.TaskID: task.GetID(),
field.Container: taskContainer.Name,
"dynamicHostPortRange": dynamicHostPortRange,
field.Error: err,
})
return nil, err
}
// Set taskContainer.ContainerPortSet to be used during network binding creation
taskContainer.SetContainerPortSet(containerPortSet)
return dockerPortMap, nil
}
// If the associated task container to this pause container is NOT the service connect AppNet container,
// we will continue to update the dockerPortMap for the pause container using the port bindings
// configured for the application container since port bindings will be published by the pasue container.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "pause"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching this. Will fix the typo when we are going in the PR to rebase against the dev branch.

containerToCheck = taskContainer
} else {
// If container is neither SC container nor pause container, it's a regular task container. Its port bindings(s)
// are published by the associated pause container, and we leave the map empty here (docker would actually complain
// otherwise).
// If the container is not a pause container, then it is a regular customers' application container
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments from line 2444-2446 are newly updated in this PR.

The original comment shown as follows is not valid; thus it is removed in this PR.

// Note - for service connect bridge mode, we do not allow customers to specify a host port for their application containers.
// Additionally, AppNet will NOT proxy traffic to that port when an ephemeral host port is assigned.

// or a service connect AppNet container. We will leave the map empty and return it as its port bindings(s)
// are published by the associated pause container.
return dockerPortMap, nil
}
}

// For each port binding config, either one of containerPort or containerPortRange is set.
// (1) containerPort is the port number on the container that's bound to the user-specified host port or the host port assigned by ECS Agent.
// (2) containerPortRange is the port number range on the container that's bound to the mapped host port range found by ECS Agent.
// (1) containerPort is the port number on the container that's bound to the user-specified host port or the
// host port assigned by ECS Agent.
// (2) containerPortRange is the port number range on the container that's bound to the mapped host port range
// found by ECS Agent.
var err error
for _, portBinding := range containerToCheck.Ports {
if portBinding.ContainerPort != 0 {
Expand All @@ -2406,7 +2461,9 @@ func (task *Task) dockerPortMap(container *apicontainer.Container, dynamicHostPo
dockerPort := nat.Port(strconv.Itoa(containerPort) + "/" + protocolStr)

if portBinding.HostPort != 0 {
// An User-specified host port exists
// An user-specified host port exists.
// Note that the host port value has been validated by ECS front end service;
// thus only an valid host port value will be streamed down to ECS Agent.
hostPortStr = strconv.Itoa(int(portBinding.HostPort))
} else {
// If there is no user-specified host port, ECS Agent will find an available host port
Expand Down Expand Up @@ -2435,7 +2492,7 @@ func (task *Task) dockerPortMap(container *apicontainer.Container, dynamicHostPo
containerToCheck.SetContainerHasPortRange(true)

containerPortRange := portBinding.ContainerPortRange
// nat.ParsePortRangeToInt validates a port range; if valid, it returns start and end ports as integers
// nat.ParsePortRangeToInt validates a port range; if valid, it returns start and end ports as integers.
startContainerPort, endContainerPort, err := nat.ParsePortRangeToInt(containerPortRange)
if err != nil {
return nil, err
Expand All @@ -2459,7 +2516,7 @@ func (task *Task) dockerPortMap(container *apicontainer.Container, dynamicHostPo
}

// For the ContainerPortRange case, append ranges to the dockerPortMap.
// nat.ParsePortSpec returns a list of port mappings in a format that docker likes
// nat.ParsePortSpec returns a list of port mappings in a format that Docker likes.
mappings, err := nat.ParsePortSpec(hostPortRange + ":" + containerPortRange + "/" + protocol)
if err != nil {
return nil, err
Expand All @@ -2470,12 +2527,12 @@ func (task *Task) dockerPortMap(container *apicontainer.Container, dynamicHostPo
}

// For the ContainerPortRange case, append containerPortRange and associated hostPortRange to the containerPortRangeMap.
// This will ensure that we consolidate range into 1 network binding while sending it to ECS
// This will ensure that we consolidate range into 1 network binding while sending it to ECS.
containerPortRangeMap[containerPortRange] = hostPortRange
}
}

// Set Container.ContainerPortSet and Container.ContainerPortRangeMap to be used during network binding creation
// Set Container.ContainerPortSet and Container.ContainerPortRangeMap to be used during network binding creation.
containerToCheck.SetContainerPortSet(containerPortSet)
containerToCheck.SetContainerPortRangeMap(containerPortRangeMap)
return dockerPortMap, nil
Expand Down
Loading