compose/ecs/pkg/amazon/backend/convert.go

327 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package backend
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
ecsapi "github.com/aws/aws-sdk-go/service/ecs"
"github.com/awslabs/goformation/v4/cloudformation"
"github.com/awslabs/goformation/v4/cloudformation/ecs"
"github.com/awslabs/goformation/v4/cloudformation/tags"
"github.com/compose-spec/compose-go/types"
"github.com/docker/cli/opts"
"github.com/docker/ecs-plugin/pkg/compose"
)
func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) {
cpu, mem, err := toLimits(service)
if err != nil {
return nil, err
}
credential := getRepoCredentials(service)
// override resolve.conf search directive to also search <project>.local
// TODO remove once ECS support hostname-only service discovery
service.Environment["LOCALDOMAIN"] = aws.String(
cloudformation.Join("", []string{
cloudformation.Ref("AWS::Region"),
".compute.internal",
fmt.Sprintf(" %s.local", project.Name),
}))
return &ecs.TaskDefinition{
ContainerDefinitions: []ecs.TaskDefinition_ContainerDefinition{
{
Command: service.Command,
DisableNetworking: service.NetworkMode == "none",
DnsSearchDomains: service.DNSSearch,
DnsServers: service.DNS,
DockerLabels: nil,
DockerSecurityOptions: service.SecurityOpt,
EntryPoint: service.Entrypoint,
Environment: toKeyValuePair(service.Environment),
Essential: true,
ExtraHosts: toHostEntryPtr(service.ExtraHosts),
FirelensConfiguration: nil,
HealthCheck: toHealthCheck(service.HealthCheck),
Hostname: service.Hostname,
Image: getImage(service.Image),
Interactive: false,
Links: nil,
LinuxParameters: toLinuxParameters(service),
LogConfiguration: &ecs.TaskDefinition_LogConfiguration{
LogDriver: ecsapi.LogDriverAwslogs,
Options: map[string]string{
"awslogs-region": cloudformation.Ref("AWS::Region"),
"awslogs-group": cloudformation.Ref("LogGroup"),
"awslogs-stream-prefix": project.Name,
},
},
Name: service.Name,
PortMappings: toPortMappings(service.Ports),
Privileged: service.Privileged,
PseudoTerminal: service.Tty,
ReadonlyRootFilesystem: service.ReadOnly,
RepositoryCredentials: credential,
ResourceRequirements: nil,
StartTimeout: 0,
StopTimeout: durationToInt(service.StopGracePeriod),
SystemControls: toSystemControls(service.Sysctls),
Ulimits: toUlimits(service.Ulimits),
User: service.User,
VolumesFrom: nil,
WorkingDirectory: service.WorkingDir,
},
},
Cpu: cpu,
Family: fmt.Sprintf("%s-%s", project.Name, service.Name),
IpcMode: service.Ipc,
Memory: mem,
NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode awsvpc.
PidMode: service.Pid,
PlacementConstraints: toPlacementConstraints(service.Deploy),
ProxyConfiguration: nil,
RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate},
Tags: toTags(service.Labels),
}, nil
}
func toTags(labels types.Labels) []tags.Tag {
t := []tags.Tag{}
for n, v := range labels {
t = append(t, tags.Tag{
Key: n,
Value: v,
})
}
return t
}
func toSystemControls(sysctls types.Mapping) []ecs.TaskDefinition_SystemControl {
sys := []ecs.TaskDefinition_SystemControl{}
for k, v := range sysctls {
sys = append(sys, ecs.TaskDefinition_SystemControl{
Namespace: k,
Value: v,
})
}
return sys
}
func toLimits(service types.ServiceConfig) (string, string, error) {
// All possible cpu/mem values for Fargate
cpuToMem := map[int64][]types.UnitBytes{
256: {512, 1024, 2048},
512: {1024, 2048, 3072, 4096},
1024: {2048, 3072, 4096, 5120, 6144, 7168, 8192},
2048: {4096, 5120, 6144, 7168, 8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384},
4096: {8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384, 17408, 18432, 19456, 20480, 21504, 22528, 23552, 24576, 25600, 26624, 27648, 28672, 29696, 30720},
}
cpuLimit := "256"
memLimit := "512"
if service.Deploy == nil {
return cpuLimit, memLimit, nil
}
limits := service.Deploy.Resources.Limits
if limits == nil {
return cpuLimit, memLimit, nil
}
if limits.NanoCPUs == "" {
return cpuLimit, memLimit, nil
}
v, err := opts.ParseCPUs(limits.NanoCPUs)
if err != nil {
return "", "", err
}
for cpu, mem := range cpuToMem {
if v <= cpu*1024*1024 {
for _, m := range mem {
if limits.MemoryBytes <= m*1024*1024 {
cpuLimit = strconv.FormatInt(cpu, 10)
memLimit = strconv.FormatInt(int64(m), 10)
return cpuLimit, memLimit, nil
}
}
}
}
return "", "", fmt.Errorf("unable to find cpu/mem for the required resources")
}
func toRequiresCompatibilities(isolation string) []*string {
if isolation == "" {
return nil
}
return []*string{&isolation}
}
func toPlacementConstraints(deploy *types.DeployConfig) []ecs.TaskDefinition_TaskDefinitionPlacementConstraint {
if deploy == nil || deploy.Placement.Constraints == nil || len(deploy.Placement.Constraints) == 0 {
return nil
}
pl := []ecs.TaskDefinition_TaskDefinitionPlacementConstraint{}
for _, c := range deploy.Placement.Constraints {
pl = append(pl, ecs.TaskDefinition_TaskDefinitionPlacementConstraint{
Expression: c,
Type: "",
})
}
return pl
}
func toPortMappings(ports []types.ServicePortConfig) []ecs.TaskDefinition_PortMapping {
if len(ports) == 0 {
return nil
}
m := []ecs.TaskDefinition_PortMapping{}
for _, p := range ports {
m = append(m, ecs.TaskDefinition_PortMapping{
ContainerPort: int(p.Target),
HostPort: int(p.Published),
Protocol: p.Protocol,
})
}
return m
}
func toUlimits(ulimits map[string]*types.UlimitsConfig) []ecs.TaskDefinition_Ulimit {
if len(ulimits) == 0 {
return nil
}
u := []ecs.TaskDefinition_Ulimit{}
for k, v := range ulimits {
u = append(u, ecs.TaskDefinition_Ulimit{
Name: k,
SoftLimit: v.Soft,
HardLimit: v.Hard,
})
}
return u
}
const Mb = 1024 * 1024
func toLinuxParameters(service types.ServiceConfig) *ecs.TaskDefinition_LinuxParameters {
return &ecs.TaskDefinition_LinuxParameters{
Capabilities: toKernelCapabilities(service.CapAdd, service.CapDrop),
Devices: nil,
InitProcessEnabled: service.Init != nil && *service.Init,
MaxSwap: 0,
// FIXME SharedMemorySize: service.ShmSize,
Swappiness: 0,
Tmpfs: toTmpfs(service.Tmpfs),
}
}
func toTmpfs(tmpfs types.StringList) []ecs.TaskDefinition_Tmpfs {
if tmpfs == nil || len(tmpfs) == 0 {
return nil
}
o := []ecs.TaskDefinition_Tmpfs{}
for _, path := range tmpfs {
o = append(o, ecs.TaskDefinition_Tmpfs{
ContainerPath: path,
Size: 100, // size is required on ECS, unlimited by the compose spec
})
}
return o
}
func toKernelCapabilities(add []string, drop []string) *ecs.TaskDefinition_KernelCapabilities {
if len(add) == 0 && len(drop) == 0 {
return nil
}
return &ecs.TaskDefinition_KernelCapabilities{
Add: add,
Drop: drop,
}
}
func toHealthCheck(check *types.HealthCheckConfig) *ecs.TaskDefinition_HealthCheck {
if check == nil {
return nil
}
retries := 0
if check.Retries != nil {
retries = int(*check.Retries)
}
return &ecs.TaskDefinition_HealthCheck{
Command: check.Test,
Interval: durationToInt(check.Interval),
Retries: retries,
StartPeriod: durationToInt(check.StartPeriod),
Timeout: durationToInt(check.Timeout),
}
}
func durationToInt(interval *types.Duration) int {
if interval == nil {
return 0
}
v := int(time.Duration(*interval).Seconds())
return v
}
func toHostEntryPtr(hosts types.HostsList) []ecs.TaskDefinition_HostEntry {
if hosts == nil || len(hosts) == 0 {
return nil
}
e := []ecs.TaskDefinition_HostEntry{}
for _, h := range hosts {
parts := strings.SplitN(h, ":", 2) // FIXME this should be handled by compose-go
e = append(e, ecs.TaskDefinition_HostEntry{
Hostname: parts[0],
IpAddress: parts[1],
})
}
return e
}
func toKeyValuePair(environment types.MappingWithEquals) []ecs.TaskDefinition_KeyValuePair {
if environment == nil || len(environment) == 0 {
return nil
}
pairs := []ecs.TaskDefinition_KeyValuePair{}
for k, v := range environment {
name := k
var value string
if v != nil {
value = *v
}
pairs = append(pairs, ecs.TaskDefinition_KeyValuePair{
Name: name,
Value: value,
})
}
return pairs
}
func getImage(image string) string {
switch f := strings.Split(image, "/"); len(f) {
case 1:
return "docker.io/library/" + image
case 2:
return "docker.io/" + image
default:
return image
}
}
func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials {
// extract registry and namespace string from image name
for key, value := range service.Extensions {
if key == compose.ExtensionPullCredentials {
return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)}
}
}
return nil
}