convert.go 14 KB

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