convert.go 16 KB

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