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

feature: container port range mapping (merging into dev) #3506

Merged
merged 17 commits into from
Dec 5, 2022
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
1 change: 1 addition & 0 deletions agent/acs/model/api/api-2.json
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@
"type":"structure",
"members":{
"containerPort":{"shape":"Integer"},
"containerPortRange":{"shape":"String"},
"hostPort":{"shape":"Integer"},
"protocol":{"shape":"TransportProtocol"}
}
Expand Down
2 changes: 2 additions & 0 deletions agent/acs/model/ecsacs/api.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions agent/api/container/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,13 @@ type Container struct {
finishedAt time.Time

labels map[string]string

// ContainerHasPortRange is set to true when the container has at least 1 port range requested.
ContainerHasPortRange bool
// ContainerPortSet is a set of singular container ports that don't belong to a containerPortRange request
ContainerPortSet map[int]struct{}
// ContainerPortRangeMap is a map of containerPortRange to its associated hostPortRange
ContainerPortRangeMap map[string]string
}

type DependsOn struct {
Expand Down Expand Up @@ -1400,3 +1407,39 @@ func (c *Container) IsContainerTornDown() bool {
defer c.lock.RUnlock()
return c.ContainerTornDownUnsafe
}

func (c *Container) SetContainerHasPortRange(containerHasPortRange bool) {
c.lock.Lock()
defer c.lock.Unlock()
c.ContainerHasPortRange = containerHasPortRange
}

func (c *Container) HasPortRange() bool {
c.lock.RLock()
defer c.lock.RUnlock()
return c.ContainerHasPortRange
}

func (c *Container) SetContainerPortSet(containerPortSet map[int]struct{}) {
c.lock.Lock()
defer c.lock.Unlock()
c.ContainerPortSet = containerPortSet
}

func (c *Container) GetContainerPortSet() map[int]struct{} {
c.lock.RLock()
defer c.lock.RUnlock()
return c.ContainerPortSet
}

func (c *Container) SetContainerPortRangeMap(portRangeMap map[string]string) {
c.lock.Lock()
defer c.lock.Unlock()
c.ContainerPortRangeMap = portRangeMap
}

func (c *Container) GetContainerPortRangeMap() map[string]string {
c.lock.RLock()
defer c.lock.RUnlock()
return c.ContainerPortRangeMap
}
3 changes: 3 additions & 0 deletions agent/api/container/port_binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"strconv"

apierrors "github.com/aws/amazon-ecs-agent/agent/api/errors"

"github.com/docker/go-connections/nat"
)

Expand All @@ -31,6 +32,8 @@ const (
type PortBinding struct {
// ContainerPort is the port inside the container
ContainerPort uint16
// ContainerPortRange is a range of ports exposed inside the container
ContainerPortRange string
// HostPort is the port exposed on the host
HostPort uint16
// BindIP is the IP address to which the port is bound
Expand Down
1 change: 1 addition & 0 deletions agent/api/container/port_binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"testing"

apierrors "github.com/aws/amazon-ecs-agent/agent/api/errors"

"github.com/docker/go-connections/nat"
)

Expand Down
102 changes: 86 additions & 16 deletions agent/api/ecsclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ import (
"strings"
"time"

"github.com/aws/amazon-ecs-agent/agent/logger"

"github.com/aws/amazon-ecs-agent/agent/api"
apicontainerstatus "github.com/aws/amazon-ecs-agent/agent/api/container/status"
apierrors "github.com/aws/amazon-ecs-agent/agent/api/errors"
Expand All @@ -30,12 +28,15 @@ import (
"github.com/aws/amazon-ecs-agent/agent/ec2"
"github.com/aws/amazon-ecs-agent/agent/ecs_client/model/ecs"
"github.com/aws/amazon-ecs-agent/agent/httpclient"
"github.com/aws/amazon-ecs-agent/agent/logger"
"github.com/aws/amazon-ecs-agent/agent/utils"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/cihub/seelog"
"github.com/docker/docker/pkg/system"
"github.com/docker/go-connections/nat"
)

const (
Expand All @@ -49,6 +50,9 @@ const (
osTypeAttrName = "ecs.os-type"
osFamilyAttrName = "ecs.os-family"
RoundtripTimeout = 5 * time.Second
// ecsMaxNetworkBindingsLength is the maximum length of the ecs.NetworkBindings list sent as part of the
// container state change payload. Currently, this is enforced only when containerPortRanges are requested.
ecsMaxNetworkBindingsLength = 100
)

// APIECSClient implements ECSClient
Expand Down Expand Up @@ -419,7 +423,12 @@ func (client *APIECSClient) SubmitTaskStateChange(change api.TaskStateChange) er

containerEvents := make([]*ecs.ContainerStateChange, len(change.Containers))
for i, containerEvent := range change.Containers {
containerEvents[i] = client.buildContainerStateChangePayload(containerEvent, client.config.ShouldExcludeIPv6PortBinding.Enabled())
payload, err := client.buildContainerStateChangePayload(containerEvent, client.config.ShouldExcludeIPv6PortBinding.Enabled())
if err != nil {
seelog.Errorf("Could not submit task state change: [%s]: %v", change.String(), err)
return err
}
containerEvents[i] = payload
}

req.Containers = containerEvents
Expand Down Expand Up @@ -460,7 +469,7 @@ func (client *APIECSClient) buildManagedAgentStateChangePayload(change api.Manag
}
}

func (client *APIECSClient) buildContainerStateChangePayload(change api.ContainerStateChange, shouldExcludeIPv6PortBinding bool) *ecs.ContainerStateChange {
func (client *APIECSClient) buildContainerStateChangePayload(change api.ContainerStateChange, shouldExcludeIPv6PortBinding bool) (*ecs.ContainerStateChange, error) {
statechange := &ecs.ContainerStateChange{
ContainerName: aws.String(change.ContainerName),
}
Expand All @@ -481,7 +490,7 @@ func (client *APIECSClient) buildContainerStateChangePayload(change api.Containe
if status != apicontainerstatus.ContainerStopped && status != apicontainerstatus.ContainerRunning {
seelog.Warnf("Not submitting unsupported upstream container state %s for container %s in task %s",
status.String(), change.ContainerName, change.TaskArn)
return nil
return nil, nil
}
stat := change.Status.String()
if stat == "DEAD" {
Expand All @@ -494,7 +503,38 @@ func (client *APIECSClient) buildContainerStateChangePayload(change api.Containe
statechange.ExitCode = aws.Int64(exitCode)
}

networkBindings := getNetworkBindings(change, shouldExcludeIPv6PortBinding)
// we enforce a limit on the no. of network bindings for containers with at-least 1 port range requested.
// this limit is enforced by ECS, and we fail early and don't call SubmitContainerStateChange.
if change.Container.HasPortRange() && len(networkBindings) > ecsMaxNetworkBindingsLength {
return nil, fmt.Errorf("no. of network bindings %v is more than the maximum supported no. %v, "+
"container: %s "+"task: %s", len(networkBindings), ecsMaxNetworkBindingsLength, change.ContainerName, change.TaskArn)
}
statechange.NetworkBindings = networkBindings

return statechange, nil
}

// ProtocolBindIP used to store protocol and bindIP information associated to a particular host port
type ProtocolBindIP struct {
protocol string
bindIP string
}

// getNetworkBindings returns the list of networkingBindings, sent to ECS as part of the container state change payload
func getNetworkBindings(change api.ContainerStateChange, shouldExcludeIPv6PortBinding bool) []*ecs.NetworkBinding {
networkBindings := []*ecs.NetworkBinding{}
// hostPortToProtocolBindIPMap is a map to store protocol and bindIP information associated to host ports
// that belong to a range. This is used in case when there are multiple protocol/bindIP combinations associated to a
// port binding. example: when both IPv4 and IPv6 bindIPs are populated by docker and shouldExcludeIPv6PortBinding is false.
hostPortToProtocolBindIPMap := map[int64][]ProtocolBindIP{}

// ContainerPortSet consists of singular ports, and ports that belong to a range, but for which we were not able to
// find contiguous host ports and ask docker to pick instead.
containerPortSet := change.Container.GetContainerPortSet()
// each entry in the ContainerPortRangeMap implies that we found a contiguous host port range for the same
containerPortRangeMap := change.Container.GetContainerPortRangeMap()

for _, binding := range change.PortBindings {
if binding.BindIP == "::" && shouldExcludeIPv6PortBinding {
seelog.Debugf("Exclude IPv6 port binding %v for container %s in task %s", binding, change.ContainerName, change.TaskArn)
Expand All @@ -506,24 +546,54 @@ func (client *APIECSClient) buildContainerStateChangePayload(change api.Containe
bindIP := binding.BindIP
protocol := binding.Protocol.String()

networkBindings = append(networkBindings, &ecs.NetworkBinding{
BindIP: aws.String(bindIP),
ContainerPort: aws.Int64(containerPort),
HostPort: aws.Int64(hostPort),
Protocol: aws.String(protocol),
})
// create network binding for each containerPort that exists in the singular ContainerPortSet
// for container ports that belong to a range, we'll have 1 consolidated network binding for the range
if _, ok := containerPortSet[int(containerPort)]; ok {
networkBindings = append(networkBindings, &ecs.NetworkBinding{
BindIP: aws.String(bindIP),
ContainerPort: aws.Int64(containerPort),
HostPort: aws.Int64(hostPort),
Protocol: aws.String(protocol),
})
} else {
// populate hostPortToProtocolBindIPMap – this is used below when we construct network binding for ranges.
hostPortToProtocolBindIPMap[hostPort] = append(hostPortToProtocolBindIPMap[hostPort],
ProtocolBindIP{
protocol: protocol,
bindIP: bindIP,
})
}
}
statechange.NetworkBindings = networkBindings

return statechange
for containerPortRange, hostPortRange := range containerPortRangeMap {
// we check for protocol and bindIP information associated to any one of the host ports from the hostPortRange,
// all ports belonging to the same range share this information.
hostPort, _, _ := nat.ParsePortRangeToInt(hostPortRange)
if val, ok := hostPortToProtocolBindIPMap[int64(hostPort)]; ok {
for _, v := range val {
networkBindings = append(networkBindings, &ecs.NetworkBinding{
BindIP: aws.String(v.bindIP),
ContainerPortRange: aws.String(containerPortRange),
HostPortRange: aws.String(hostPortRange),
Protocol: aws.String(v.protocol),
})
}
}
}

return networkBindings
}

func (client *APIECSClient) SubmitContainerStateChange(change api.ContainerStateChange) error {
pl := client.buildContainerStateChangePayload(change, client.config.ShouldExcludeIPv6PortBinding.Enabled())
if pl == nil {
pl, err := client.buildContainerStateChangePayload(change, client.config.ShouldExcludeIPv6PortBinding.Enabled())
if err != nil {
seelog.Errorf("Could not build container state change payload: [%s]: %v", change.String(), err)
return err
} else if pl == nil {
return nil
}
_, err := client.submitStateChangeClient.SubmitContainerStateChange(&ecs.SubmitContainerStateChangeInput{

_, err = client.submitStateChangeClient.SubmitContainerStateChange(&ecs.SubmitContainerStateChangeInput{
Cluster: aws.String(client.config.Cluster),
ContainerName: aws.String(change.ContainerName),
ExitCode: pl.ExitCode,
Expand Down
Loading