convert.go 17 KB

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