convert.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. package backend
  2. import (
  3. "fmt"
  4. "strconv"
  5. "strings"
  6. "time"
  7. "github.com/aws/aws-sdk-go/aws"
  8. ecsapi "github.com/aws/aws-sdk-go/service/ecs"
  9. "github.com/awslabs/goformation/v4/cloudformation"
  10. "github.com/awslabs/goformation/v4/cloudformation/ecs"
  11. "github.com/awslabs/goformation/v4/cloudformation/tags"
  12. "github.com/compose-spec/compose-go/types"
  13. "github.com/docker/cli/opts"
  14. "github.com/docker/ecs-plugin/pkg/compose"
  15. )
  16. func Convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) {
  17. cpu, mem, err := toLimits(service)
  18. if err != nil {
  19. return nil, err
  20. }
  21. credential := getRepoCredentials(service)
  22. // override resolve.conf search directive to also search <project>.local
  23. // TODO remove once ECS support hostname-only service discovery
  24. service.Environment["LOCALDOMAIN"] = aws.String(
  25. cloudformation.Join("", []string{
  26. cloudformation.Ref("AWS::Region"),
  27. ".compute.internal",
  28. fmt.Sprintf(" %s.local", project.Name),
  29. }))
  30. return &ecs.TaskDefinition{
  31. ContainerDefinitions: []ecs.TaskDefinition_ContainerDefinition{
  32. {
  33. Command: service.Command,
  34. DisableNetworking: service.NetworkMode == "none",
  35. DnsSearchDomains: service.DNSSearch,
  36. DnsServers: service.DNS,
  37. DockerLabels: nil,
  38. DockerSecurityOptions: service.SecurityOpt,
  39. EntryPoint: service.Entrypoint,
  40. Environment: toKeyValuePair(service.Environment),
  41. Essential: true,
  42. ExtraHosts: toHostEntryPtr(service.ExtraHosts),
  43. FirelensConfiguration: nil,
  44. HealthCheck: toHealthCheck(service.HealthCheck),
  45. Hostname: service.Hostname,
  46. Image: service.Image,
  47. Interactive: false,
  48. Links: nil,
  49. LinuxParameters: toLinuxParameters(service),
  50. LogConfiguration: &ecs.TaskDefinition_LogConfiguration{
  51. LogDriver: ecsapi.LogDriverAwslogs,
  52. Options: map[string]string{
  53. "awslogs-region": cloudformation.Ref("AWS::Region"),
  54. "awslogs-group": cloudformation.Ref("LogGroup"),
  55. "awslogs-stream-prefix": project.Name,
  56. },
  57. },
  58. Name: service.Name,
  59. PortMappings: toPortMappings(service.Ports),
  60. Privileged: service.Privileged,
  61. PseudoTerminal: service.Tty,
  62. ReadonlyRootFilesystem: service.ReadOnly,
  63. RepositoryCredentials: credential,
  64. ResourceRequirements: nil,
  65. StartTimeout: 0,
  66. StopTimeout: durationToInt(service.StopGracePeriod),
  67. SystemControls: toSystemControls(service.Sysctls),
  68. Ulimits: toUlimits(service.Ulimits),
  69. User: service.User,
  70. VolumesFrom: nil,
  71. WorkingDirectory: service.WorkingDir,
  72. },
  73. },
  74. Cpu: cpu,
  75. Family: fmt.Sprintf("%s-%s", project.Name, service.Name),
  76. IpcMode: service.Ipc,
  77. Memory: mem,
  78. NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’.
  79. PidMode: service.Pid,
  80. PlacementConstraints: toPlacementConstraints(service.Deploy),
  81. ProxyConfiguration: nil,
  82. RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate},
  83. Tags: toTags(service.Labels),
  84. }, nil
  85. }
  86. func toTags(labels types.Labels) []tags.Tag {
  87. t := []tags.Tag{}
  88. for n, v := range labels {
  89. t = append(t, tags.Tag{
  90. Key: n,
  91. Value: v,
  92. })
  93. }
  94. return t
  95. }
  96. func toSystemControls(sysctls types.Mapping) []ecs.TaskDefinition_SystemControl {
  97. sys := []ecs.TaskDefinition_SystemControl{}
  98. for k, v := range sysctls {
  99. sys = append(sys, ecs.TaskDefinition_SystemControl{
  100. Namespace: k,
  101. Value: v,
  102. })
  103. }
  104. return sys
  105. }
  106. func toLimits(service types.ServiceConfig) (string, string, error) {
  107. // All possible cpu/mem values for Fargate
  108. cpuToMem := map[int64][]types.UnitBytes{
  109. 256: {512, 1024, 2048},
  110. 512: {1024, 2048, 3072, 4096},
  111. 1024: {2048, 3072, 4096, 5120, 6144, 7168, 8192},
  112. 2048: {4096, 5120, 6144, 7168, 8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384},
  113. 4096: {8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384, 17408, 18432, 19456, 20480, 21504, 22528, 23552, 24576, 25600, 26624, 27648, 28672, 29696, 30720},
  114. }
  115. cpuLimit := "256"
  116. memLimit := "512"
  117. if service.Deploy == nil {
  118. return cpuLimit, memLimit, nil
  119. }
  120. limits := service.Deploy.Resources.Limits
  121. if limits == nil {
  122. return cpuLimit, memLimit, nil
  123. }
  124. if limits.NanoCPUs == "" {
  125. return cpuLimit, memLimit, nil
  126. }
  127. v, err := opts.ParseCPUs(limits.NanoCPUs)
  128. if err != nil {
  129. return "", "", err
  130. }
  131. for cpu, mem := range cpuToMem {
  132. if v <= cpu*1024*1024 {
  133. for _, m := range mem {
  134. if limits.MemoryBytes <= m*1024*1024 {
  135. cpuLimit = strconv.FormatInt(cpu, 10)
  136. memLimit = strconv.FormatInt(int64(m), 10)
  137. return cpuLimit, memLimit, nil
  138. }
  139. }
  140. }
  141. }
  142. return "", "", fmt.Errorf("unable to find cpu/mem for the required resources")
  143. }
  144. func toRequiresCompatibilities(isolation string) []*string {
  145. if isolation == "" {
  146. return nil
  147. }
  148. return []*string{&isolation}
  149. }
  150. func toPlacementConstraints(deploy *types.DeployConfig) []ecs.TaskDefinition_TaskDefinitionPlacementConstraint {
  151. if deploy == nil || deploy.Placement.Constraints == nil || len(deploy.Placement.Constraints) == 0 {
  152. return nil
  153. }
  154. pl := []ecs.TaskDefinition_TaskDefinitionPlacementConstraint{}
  155. for _, c := range deploy.Placement.Constraints {
  156. pl = append(pl, ecs.TaskDefinition_TaskDefinitionPlacementConstraint{
  157. Expression: c,
  158. Type: "",
  159. })
  160. }
  161. return pl
  162. }
  163. func toPortMappings(ports []types.ServicePortConfig) []ecs.TaskDefinition_PortMapping {
  164. if len(ports) == 0 {
  165. return nil
  166. }
  167. m := []ecs.TaskDefinition_PortMapping{}
  168. for _, p := range ports {
  169. m = append(m, ecs.TaskDefinition_PortMapping{
  170. ContainerPort: int(p.Target),
  171. HostPort: int(p.Published),
  172. Protocol: p.Protocol,
  173. })
  174. }
  175. return m
  176. }
  177. func toUlimits(ulimits map[string]*types.UlimitsConfig) []ecs.TaskDefinition_Ulimit {
  178. if len(ulimits) == 0 {
  179. return nil
  180. }
  181. u := []ecs.TaskDefinition_Ulimit{}
  182. for k, v := range ulimits {
  183. u = append(u, ecs.TaskDefinition_Ulimit{
  184. Name: k,
  185. SoftLimit: v.Soft,
  186. HardLimit: v.Hard,
  187. })
  188. }
  189. return u
  190. }
  191. const Mb = 1024 * 1024
  192. func toLinuxParameters(service types.ServiceConfig) *ecs.TaskDefinition_LinuxParameters {
  193. return &ecs.TaskDefinition_LinuxParameters{
  194. Capabilities: toKernelCapabilities(service.CapAdd, service.CapDrop),
  195. Devices: nil,
  196. InitProcessEnabled: service.Init != nil && *service.Init,
  197. MaxSwap: 0,
  198. // FIXME SharedMemorySize: service.ShmSize,
  199. Swappiness: 0,
  200. Tmpfs: toTmpfs(service.Tmpfs),
  201. }
  202. }
  203. func toTmpfs(tmpfs types.StringList) []ecs.TaskDefinition_Tmpfs {
  204. if tmpfs == nil || len(tmpfs) == 0 {
  205. return nil
  206. }
  207. o := []ecs.TaskDefinition_Tmpfs{}
  208. for _, path := range tmpfs {
  209. o = append(o, ecs.TaskDefinition_Tmpfs{
  210. ContainerPath: path,
  211. Size: 100, // size is required on ECS, unlimited by the compose spec
  212. })
  213. }
  214. return o
  215. }
  216. func toKernelCapabilities(add []string, drop []string) *ecs.TaskDefinition_KernelCapabilities {
  217. if len(add) == 0 && len(drop) == 0 {
  218. return nil
  219. }
  220. return &ecs.TaskDefinition_KernelCapabilities{
  221. Add: add,
  222. Drop: drop,
  223. }
  224. }
  225. func toHealthCheck(check *types.HealthCheckConfig) *ecs.TaskDefinition_HealthCheck {
  226. if check == nil {
  227. return nil
  228. }
  229. retries := 0
  230. if check.Retries != nil {
  231. retries = int(*check.Retries)
  232. }
  233. return &ecs.TaskDefinition_HealthCheck{
  234. Command: check.Test,
  235. Interval: durationToInt(check.Interval),
  236. Retries: retries,
  237. StartPeriod: durationToInt(check.StartPeriod),
  238. Timeout: durationToInt(check.Timeout),
  239. }
  240. }
  241. func durationToInt(interval *types.Duration) int {
  242. if interval == nil {
  243. return 0
  244. }
  245. v := int(time.Duration(*interval).Seconds())
  246. return v
  247. }
  248. func toHostEntryPtr(hosts types.HostsList) []ecs.TaskDefinition_HostEntry {
  249. if hosts == nil || len(hosts) == 0 {
  250. return nil
  251. }
  252. e := []ecs.TaskDefinition_HostEntry{}
  253. for _, h := range hosts {
  254. parts := strings.SplitN(h, ":", 2) // FIXME this should be handled by compose-go
  255. e = append(e, ecs.TaskDefinition_HostEntry{
  256. Hostname: parts[0],
  257. IpAddress: parts[1],
  258. })
  259. }
  260. return e
  261. }
  262. func toKeyValuePair(environment types.MappingWithEquals) []ecs.TaskDefinition_KeyValuePair {
  263. if environment == nil || len(environment) == 0 {
  264. return nil
  265. }
  266. pairs := []ecs.TaskDefinition_KeyValuePair{}
  267. for k, v := range environment {
  268. name := k
  269. var value string
  270. if v != nil {
  271. value = *v
  272. }
  273. pairs = append(pairs, ecs.TaskDefinition_KeyValuePair{
  274. Name: name,
  275. Value: value,
  276. })
  277. }
  278. return pairs
  279. }
  280. func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials {
  281. // extract registry and namespace string from image name
  282. for key, value := range service.Extensions {
  283. if key == compose.ExtensionPullCredentials {
  284. return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)}
  285. }
  286. }
  287. return nil
  288. }