convert.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. /*
  2. Copyright 2020 Docker, Inc.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package ecs
  14. import (
  15. "encoding/json"
  16. "fmt"
  17. "os"
  18. "path/filepath"
  19. "sort"
  20. "strconv"
  21. "strings"
  22. "time"
  23. "github.com/aws/aws-sdk-go/aws"
  24. ecsapi "github.com/aws/aws-sdk-go/service/ecs"
  25. "github.com/awslabs/goformation/v4/cloudformation"
  26. "github.com/awslabs/goformation/v4/cloudformation/ecs"
  27. "github.com/compose-spec/compose-go/types"
  28. "github.com/docker/cli/opts"
  29. "github.com/joho/godotenv"
  30. "github.com/docker/api/ecs/secrets"
  31. )
  32. const secretsInitContainerImage = "docker/ecs-secrets-sidecar"
  33. func convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) {
  34. cpu, mem, err := toLimits(service)
  35. if err != nil {
  36. return nil, err
  37. }
  38. _, memReservation, err := toContainerReservation(service)
  39. if err != nil {
  40. return nil, err
  41. }
  42. credential := getRepoCredentials(service)
  43. // override resolve.conf search directive to also search <project>.local
  44. // TODO remove once ECS support hostname-only service discovery
  45. service.Environment["LOCALDOMAIN"] = aws.String(
  46. cloudformation.Join("", []string{
  47. cloudformation.Ref("AWS::Region"),
  48. ".compute.internal",
  49. fmt.Sprintf(" %s.local", project.Name),
  50. }))
  51. logConfiguration := getLogConfiguration(service, project)
  52. var (
  53. initContainers []ecs.TaskDefinition_ContainerDefinition
  54. volumes []ecs.TaskDefinition_Volume
  55. mounts []ecs.TaskDefinition_MountPoint
  56. )
  57. if len(service.Secrets) > 0 {
  58. secretsVolume, secretsMount, secretsSideCar, err := createSecretsSideCar(project, service, logConfiguration)
  59. if err != nil {
  60. return nil, err
  61. }
  62. initContainers = append(initContainers, secretsSideCar)
  63. volumes = append(volumes, secretsVolume)
  64. mounts = append(mounts, secretsMount)
  65. }
  66. var dependencies []ecs.TaskDefinition_ContainerDependency
  67. for _, c := range initContainers {
  68. dependencies = append(dependencies, ecs.TaskDefinition_ContainerDependency{
  69. Condition: ecsapi.ContainerConditionSuccess,
  70. ContainerName: c.Name,
  71. })
  72. }
  73. pairs, err := createEnvironment(project, service)
  74. if err != nil {
  75. return nil, err
  76. }
  77. containers := append(initContainers, ecs.TaskDefinition_ContainerDefinition{
  78. Command: service.Command,
  79. DisableNetworking: service.NetworkMode == "none",
  80. DependsOnProp: dependencies,
  81. DnsSearchDomains: service.DNSSearch,
  82. DnsServers: service.DNS,
  83. DockerSecurityOptions: service.SecurityOpt,
  84. EntryPoint: service.Entrypoint,
  85. Environment: pairs,
  86. Essential: true,
  87. ExtraHosts: toHostEntryPtr(service.ExtraHosts),
  88. FirelensConfiguration: nil,
  89. HealthCheck: toHealthCheck(service.HealthCheck),
  90. Hostname: service.Hostname,
  91. Image: service.Image,
  92. Interactive: false,
  93. Links: nil,
  94. LinuxParameters: toLinuxParameters(service),
  95. LogConfiguration: logConfiguration,
  96. MemoryReservation: memReservation,
  97. MountPoints: mounts,
  98. Name: service.Name,
  99. PortMappings: toPortMappings(service.Ports),
  100. Privileged: service.Privileged,
  101. PseudoTerminal: service.Tty,
  102. ReadonlyRootFilesystem: service.ReadOnly,
  103. RepositoryCredentials: credential,
  104. ResourceRequirements: nil,
  105. StartTimeout: 0,
  106. StopTimeout: durationToInt(service.StopGracePeriod),
  107. SystemControls: toSystemControls(service.Sysctls),
  108. Ulimits: toUlimits(service.Ulimits),
  109. User: service.User,
  110. VolumesFrom: nil,
  111. WorkingDirectory: service.WorkingDir,
  112. })
  113. return &ecs.TaskDefinition{
  114. ContainerDefinitions: containers,
  115. Cpu: cpu,
  116. Family: fmt.Sprintf("%s-%s", project.Name, service.Name),
  117. IpcMode: service.Ipc,
  118. Memory: mem,
  119. NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’.
  120. PidMode: service.Pid,
  121. PlacementConstraints: toPlacementConstraints(service.Deploy),
  122. ProxyConfiguration: nil,
  123. RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate},
  124. Volumes: volumes,
  125. }, nil
  126. }
  127. func createSecretsSideCar(project *types.Project, service types.ServiceConfig, logConfiguration *ecs.TaskDefinition_LogConfiguration) (ecs.TaskDefinition_Volume, ecs.TaskDefinition_MountPoint, ecs.TaskDefinition_ContainerDefinition, error) {
  128. initContainerName := fmt.Sprintf("%s_Secrets_InitContainer", normalizeResourceName(service.Name))
  129. secretsVolume := ecs.TaskDefinition_Volume{
  130. Name: "secrets",
  131. }
  132. secretsMount := ecs.TaskDefinition_MountPoint{
  133. ContainerPath: "/run/secrets/",
  134. ReadOnly: true,
  135. SourceVolume: "secrets",
  136. }
  137. var (
  138. args []secrets.Secret
  139. taskSecrets []ecs.TaskDefinition_Secret
  140. )
  141. for _, s := range service.Secrets {
  142. secretConfig := project.Secrets[s.Source]
  143. if s.Target == "" {
  144. s.Target = s.Source
  145. }
  146. taskSecrets = append(taskSecrets, ecs.TaskDefinition_Secret{
  147. Name: s.Target,
  148. ValueFrom: secretConfig.Name,
  149. })
  150. var keys []string
  151. if ext, ok := secretConfig.Extensions[ExtensionKeys]; ok {
  152. if key, ok := ext.(string); ok {
  153. keys = append(keys, key)
  154. } else {
  155. for _, k := range ext.([]interface{}) {
  156. keys = append(keys, k.(string))
  157. }
  158. }
  159. }
  160. args = append(args, secrets.Secret{
  161. Name: s.Target,
  162. Keys: keys,
  163. })
  164. }
  165. command, err := json.Marshal(args)
  166. if err != nil {
  167. return ecs.TaskDefinition_Volume{}, ecs.TaskDefinition_MountPoint{}, ecs.TaskDefinition_ContainerDefinition{}, err
  168. }
  169. secretsSideCar := ecs.TaskDefinition_ContainerDefinition{
  170. Name: initContainerName,
  171. Image: secretsInitContainerImage,
  172. Command: []string{string(command)},
  173. Essential: false, // FIXME this will be ignored, see https://github.com/awslabs/goformation/issues/61#issuecomment-625139607
  174. LogConfiguration: logConfiguration,
  175. MountPoints: []ecs.TaskDefinition_MountPoint{
  176. {
  177. ContainerPath: "/run/secrets/",
  178. ReadOnly: false,
  179. SourceVolume: "secrets",
  180. },
  181. },
  182. Secrets: taskSecrets,
  183. }
  184. return secretsVolume, secretsMount, secretsSideCar, nil
  185. }
  186. func createEnvironment(project *types.Project, service types.ServiceConfig) ([]ecs.TaskDefinition_KeyValuePair, error) {
  187. environment := map[string]*string{}
  188. for _, f := range service.EnvFile {
  189. if !filepath.IsAbs(f) {
  190. f = filepath.Join(project.WorkingDir, f)
  191. }
  192. if _, err := os.Stat(f); os.IsNotExist(err) {
  193. return nil, err
  194. }
  195. file, err := os.Open(f)
  196. if err != nil {
  197. return nil, err
  198. }
  199. defer file.Close() // nolint:errcheck
  200. env, err := godotenv.Parse(file)
  201. if err != nil {
  202. return nil, err
  203. }
  204. for k, v := range env {
  205. environment[k] = &v
  206. }
  207. }
  208. for k, v := range service.Environment {
  209. environment[k] = v
  210. }
  211. var pairs []ecs.TaskDefinition_KeyValuePair
  212. for k, v := range environment {
  213. name := k
  214. var value string
  215. if v != nil {
  216. value = *v
  217. }
  218. pairs = append(pairs, ecs.TaskDefinition_KeyValuePair{
  219. Name: name,
  220. Value: value,
  221. })
  222. }
  223. return pairs, nil
  224. }
  225. func getLogConfiguration(service types.ServiceConfig, project *types.Project) *ecs.TaskDefinition_LogConfiguration {
  226. options := map[string]string{
  227. "awslogs-region": cloudformation.Ref("AWS::Region"),
  228. "awslogs-group": cloudformation.Ref("LogGroup"),
  229. "awslogs-stream-prefix": project.Name,
  230. }
  231. if service.Logging != nil {
  232. for k, v := range service.Logging.Options {
  233. if strings.HasPrefix(k, "awslogs-") {
  234. options[k] = v
  235. }
  236. }
  237. }
  238. logConfiguration := &ecs.TaskDefinition_LogConfiguration{
  239. LogDriver: ecsapi.LogDriverAwslogs,
  240. Options: options,
  241. }
  242. return logConfiguration
  243. }
  244. func toSystemControls(sysctls types.Mapping) []ecs.TaskDefinition_SystemControl {
  245. sys := []ecs.TaskDefinition_SystemControl{}
  246. for k, v := range sysctls {
  247. sys = append(sys, ecs.TaskDefinition_SystemControl{
  248. Namespace: k,
  249. Value: v,
  250. })
  251. }
  252. return sys
  253. }
  254. const MiB = 1024 * 1024
  255. func toLimits(service types.ServiceConfig) (string, string, error) {
  256. // All possible cpu/mem values for Fargate
  257. cpuToMem := map[int64][]types.UnitBytes{
  258. 256: {512, 1024, 2048},
  259. 512: {1024, 2048, 3072, 4096},
  260. 1024: {2048, 3072, 4096, 5120, 6144, 7168, 8192},
  261. 2048: {4096, 5120, 6144, 7168, 8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384},
  262. 4096: {8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384, 17408, 18432, 19456, 20480, 21504, 22528, 23552, 24576, 25600, 26624, 27648, 28672, 29696, 30720},
  263. }
  264. cpuLimit := "256"
  265. memLimit := "512"
  266. if service.Deploy == nil {
  267. return cpuLimit, memLimit, nil
  268. }
  269. limits := service.Deploy.Resources.Limits
  270. if limits == nil {
  271. return cpuLimit, memLimit, nil
  272. }
  273. if limits.NanoCPUs == "" {
  274. return cpuLimit, memLimit, nil
  275. }
  276. v, err := opts.ParseCPUs(limits.NanoCPUs)
  277. if err != nil {
  278. return "", "", err
  279. }
  280. var cpus []int64
  281. for k := range cpuToMem {
  282. cpus = append(cpus, k)
  283. }
  284. sort.Slice(cpus, func(i, j int) bool { return cpus[i] < cpus[j] })
  285. for _, cpu := range cpus {
  286. mem := cpuToMem[cpu]
  287. if v <= cpu*MiB {
  288. for _, m := range mem {
  289. if limits.MemoryBytes <= m*MiB {
  290. cpuLimit = strconv.FormatInt(cpu, 10)
  291. memLimit = strconv.FormatInt(int64(m), 10)
  292. return cpuLimit, memLimit, nil
  293. }
  294. }
  295. }
  296. }
  297. return "", "", fmt.Errorf("the resources requested are not supported by ECS/Fargate")
  298. }
  299. func toContainerReservation(service types.ServiceConfig) (string, int, error) {
  300. cpuReservation := ".0"
  301. memReservation := 0
  302. if service.Deploy == nil {
  303. return cpuReservation, memReservation, nil
  304. }
  305. reservations := service.Deploy.Resources.Reservations
  306. if reservations == nil {
  307. return cpuReservation, memReservation, nil
  308. }
  309. return reservations.NanoCPUs, int(reservations.MemoryBytes / MiB), nil
  310. }
  311. func toPlacementConstraints(deploy *types.DeployConfig) []ecs.TaskDefinition_TaskDefinitionPlacementConstraint {
  312. if deploy == nil || deploy.Placement.Constraints == nil || len(deploy.Placement.Constraints) == 0 {
  313. return nil
  314. }
  315. pl := []ecs.TaskDefinition_TaskDefinitionPlacementConstraint{}
  316. for _, c := range deploy.Placement.Constraints {
  317. pl = append(pl, ecs.TaskDefinition_TaskDefinitionPlacementConstraint{
  318. Expression: c,
  319. Type: "",
  320. })
  321. }
  322. return pl
  323. }
  324. func toPortMappings(ports []types.ServicePortConfig) []ecs.TaskDefinition_PortMapping {
  325. if len(ports) == 0 {
  326. return nil
  327. }
  328. m := []ecs.TaskDefinition_PortMapping{}
  329. for _, p := range ports {
  330. m = append(m, ecs.TaskDefinition_PortMapping{
  331. ContainerPort: int(p.Target),
  332. HostPort: int(p.Published),
  333. Protocol: p.Protocol,
  334. })
  335. }
  336. return m
  337. }
  338. func toUlimits(ulimits map[string]*types.UlimitsConfig) []ecs.TaskDefinition_Ulimit {
  339. if len(ulimits) == 0 {
  340. return nil
  341. }
  342. u := []ecs.TaskDefinition_Ulimit{}
  343. for k, v := range ulimits {
  344. u = append(u, ecs.TaskDefinition_Ulimit{
  345. Name: k,
  346. SoftLimit: v.Soft,
  347. HardLimit: v.Hard,
  348. })
  349. }
  350. return u
  351. }
  352. func toLinuxParameters(service types.ServiceConfig) *ecs.TaskDefinition_LinuxParameters {
  353. return &ecs.TaskDefinition_LinuxParameters{
  354. Capabilities: toKernelCapabilities(service.CapAdd, service.CapDrop),
  355. Devices: nil,
  356. InitProcessEnabled: service.Init != nil && *service.Init,
  357. MaxSwap: 0,
  358. // FIXME SharedMemorySize: service.ShmSize,
  359. Swappiness: 0,
  360. Tmpfs: toTmpfs(service.Tmpfs),
  361. }
  362. }
  363. func toTmpfs(tmpfs types.StringList) []ecs.TaskDefinition_Tmpfs {
  364. if tmpfs == nil || len(tmpfs) == 0 {
  365. return nil
  366. }
  367. o := []ecs.TaskDefinition_Tmpfs{}
  368. for _, path := range tmpfs {
  369. o = append(o, ecs.TaskDefinition_Tmpfs{
  370. ContainerPath: path,
  371. Size: 100, // size is required on ECS, unlimited by the compose spec
  372. })
  373. }
  374. return o
  375. }
  376. func toKernelCapabilities(add []string, drop []string) *ecs.TaskDefinition_KernelCapabilities {
  377. if len(add) == 0 && len(drop) == 0 {
  378. return nil
  379. }
  380. return &ecs.TaskDefinition_KernelCapabilities{
  381. Add: add,
  382. Drop: drop,
  383. }
  384. }
  385. func toHealthCheck(check *types.HealthCheckConfig) *ecs.TaskDefinition_HealthCheck {
  386. if check == nil {
  387. return nil
  388. }
  389. retries := 0
  390. if check.Retries != nil {
  391. retries = int(*check.Retries)
  392. }
  393. return &ecs.TaskDefinition_HealthCheck{
  394. Command: check.Test,
  395. Interval: durationToInt(check.Interval),
  396. Retries: retries,
  397. StartPeriod: durationToInt(check.StartPeriod),
  398. Timeout: durationToInt(check.Timeout),
  399. }
  400. }
  401. func durationToInt(interval *types.Duration) int {
  402. if interval == nil {
  403. return 0
  404. }
  405. v := int(time.Duration(*interval).Seconds())
  406. return v
  407. }
  408. func toHostEntryPtr(hosts types.HostsList) []ecs.TaskDefinition_HostEntry {
  409. if hosts == nil || len(hosts) == 0 {
  410. return nil
  411. }
  412. e := []ecs.TaskDefinition_HostEntry{}
  413. for _, h := range hosts {
  414. parts := strings.SplitN(h, ":", 2) // FIXME this should be handled by compose-go
  415. e = append(e, ecs.TaskDefinition_HostEntry{
  416. Hostname: parts[0],
  417. IpAddress: parts[1],
  418. })
  419. }
  420. return e
  421. }
  422. func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials {
  423. // extract registry and namespace string from image name
  424. for key, value := range service.Extensions {
  425. if key == ExtensionPullCredentials {
  426. return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)}
  427. }
  428. }
  429. return nil
  430. }