cloudformation_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  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. "fmt"
  16. "reflect"
  17. "testing"
  18. "github.com/docker/compose-cli/api/compose"
  19. "github.com/aws/aws-sdk-go/service/elbv2"
  20. "github.com/awslabs/goformation/v4/cloudformation"
  21. "github.com/awslabs/goformation/v4/cloudformation/ec2"
  22. "github.com/awslabs/goformation/v4/cloudformation/ecs"
  23. "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2"
  24. "github.com/awslabs/goformation/v4/cloudformation/iam"
  25. "github.com/awslabs/goformation/v4/cloudformation/logs"
  26. "github.com/compose-spec/compose-go/cli"
  27. "github.com/compose-spec/compose-go/loader"
  28. "github.com/compose-spec/compose-go/types"
  29. "gotest.tools/v3/assert"
  30. "gotest.tools/v3/golden"
  31. )
  32. func TestSimpleConvert(t *testing.T) {
  33. project := load(t, "testdata/input/simple-single-service.yaml")
  34. result := convertResultAsString(t, project)
  35. expected := "simple/simple-cloudformation-conversion.golden"
  36. golden.Assert(t, result, expected)
  37. }
  38. func TestLogging(t *testing.T) {
  39. template := convertYaml(t, `
  40. services:
  41. foo:
  42. image: hello_world
  43. logging:
  44. options:
  45. awslogs-datetime-pattern: "FOO"
  46. x-aws-logs_retention: 10
  47. `)
  48. def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition)
  49. logging := def.ContainerDefinitions[0].LogConfiguration
  50. assert.Equal(t, logging.Options["awslogs-datetime-pattern"], "FOO")
  51. logGroup := template.Resources["LogGroup"].(*logs.LogGroup)
  52. assert.Equal(t, logGroup.RetentionInDays, 10)
  53. }
  54. func TestEnvFile(t *testing.T) {
  55. template := convertYaml(t, `
  56. services:
  57. foo:
  58. image: hello_world
  59. env_file:
  60. - testdata/input/envfile
  61. `)
  62. def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition)
  63. env := def.ContainerDefinitions[0].Environment
  64. var found bool
  65. for _, pair := range env {
  66. if pair.Name == "FOO" {
  67. assert.Equal(t, pair.Value, "BAR")
  68. found = true
  69. }
  70. }
  71. assert.Check(t, found, "environment variable FOO not set")
  72. }
  73. func TestEnvFileAndEnv(t *testing.T) {
  74. template := convertYaml(t, `
  75. services:
  76. foo:
  77. image: hello_world
  78. env_file:
  79. - testdata/input/envfile
  80. environment:
  81. - "FOO=ZOT"
  82. `)
  83. def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition)
  84. env := def.ContainerDefinitions[0].Environment
  85. var found bool
  86. for _, pair := range env {
  87. if pair.Name == "FOO" {
  88. assert.Equal(t, pair.Value, "ZOT")
  89. found = true
  90. }
  91. }
  92. assert.Check(t, found, "environment variable FOO not set")
  93. }
  94. func TestRollingUpdateLimits(t *testing.T) {
  95. template := convertYaml(t, `
  96. services:
  97. foo:
  98. image: hello_world
  99. deploy:
  100. replicas: 4
  101. update_config:
  102. parallelism: 2
  103. `)
  104. service := template.Resources["FooService"].(*ecs.Service)
  105. assert.Check(t, service.DeploymentConfiguration.MaximumPercent == 150)
  106. assert.Check(t, service.DeploymentConfiguration.MinimumHealthyPercent == 50)
  107. }
  108. func TestRollingUpdateExtension(t *testing.T) {
  109. template := convertYaml(t, `
  110. services:
  111. foo:
  112. image: hello_world
  113. deploy:
  114. update_config:
  115. x-aws-min_percent: 25
  116. x-aws-max_percent: 125
  117. `)
  118. service := template.Resources["FooService"].(*ecs.Service)
  119. assert.Check(t, service.DeploymentConfiguration.MaximumPercent == 125)
  120. assert.Check(t, service.DeploymentConfiguration.MinimumHealthyPercent == 25)
  121. }
  122. func TestRolePolicy(t *testing.T) {
  123. template := convertYaml(t, `
  124. services:
  125. foo:
  126. image: hello_world
  127. x-aws-pull_credentials: "secret"
  128. `)
  129. x := template.Resources["FooTaskExecutionRole"]
  130. assert.Check(t, x != nil)
  131. role := *(x.(*iam.Role))
  132. assert.Check(t, role.ManagedPolicyArns[0] == ecsTaskExecutionPolicy)
  133. assert.Check(t, role.ManagedPolicyArns[1] == ecrReadOnlyPolicy)
  134. // We expect an extra policy has been created for x-aws-pull_credentials
  135. assert.Check(t, len(role.Policies) == 1)
  136. policy := role.Policies[0].PolicyDocument.(*PolicyDocument)
  137. expected := []string{"secretsmanager:GetSecretValue", "ssm:GetParameters", "kms:Decrypt"}
  138. assert.DeepEqual(t, expected, policy.Statement[0].Action)
  139. assert.DeepEqual(t, []string{"secret"}, policy.Statement[0].Resource)
  140. }
  141. func TestMapNetworksToSecurityGroups(t *testing.T) {
  142. template := convertYaml(t, `
  143. services:
  144. test:
  145. image: hello_world
  146. networks:
  147. - front-tier
  148. - back-tier
  149. networks:
  150. front-tier:
  151. name: public
  152. back-tier:
  153. internal: true
  154. `)
  155. assert.Check(t, template.Resources["TestPublicNetwork"] != nil)
  156. assert.Check(t, template.Resources["TestBacktierNetwork"] != nil)
  157. assert.Check(t, template.Resources["TestBacktierNetworkIngress"] != nil)
  158. i := template.Resources["TestPublicNetworkIngress"]
  159. assert.Check(t, i != nil)
  160. ingress := *i.(*ec2.SecurityGroupIngress)
  161. assert.Check(t, ingress.SourceSecurityGroupId == cloudformation.Ref("TestPublicNetwork"))
  162. }
  163. func TestLoadBalancerTypeApplication(t *testing.T) {
  164. cases := []string{
  165. `services:
  166. test:
  167. image: nginx
  168. ports:
  169. - 80:80
  170. `,
  171. `services:
  172. test:
  173. image: nginx
  174. ports:
  175. - target: 8080
  176. protocol: http
  177. `,
  178. `services:
  179. test:
  180. image: nginx
  181. ports:
  182. - target: 8080
  183. x-aws-protocol: http
  184. `,
  185. }
  186. for _, y := range cases {
  187. template := convertYaml(t, y)
  188. lb := template.Resources["TestLoadBalancer"]
  189. assert.Check(t, lb != nil)
  190. loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
  191. assert.Check(t, len(loadBalancer.Name) <= 32)
  192. assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumApplication)
  193. assert.Check(t, len(loadBalancer.SecurityGroups) > 0)
  194. }
  195. }
  196. func TestNoLoadBalancerIfNoPortExposed(t *testing.T) {
  197. template := convertYaml(t, `
  198. services:
  199. test:
  200. image: nginx
  201. foo:
  202. image: bar
  203. `)
  204. for _, r := range template.Resources {
  205. assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::TargetGroup")
  206. assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::Listener")
  207. assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::PortPublisher")
  208. }
  209. }
  210. func TestServiceReplicas(t *testing.T) {
  211. template := convertYaml(t, `
  212. services:
  213. test:
  214. image: nginx
  215. deploy:
  216. replicas: 10
  217. `)
  218. s := template.Resources["TestService"]
  219. assert.Check(t, s != nil)
  220. service := *s.(*ecs.Service)
  221. assert.Check(t, service.DesiredCount == 10)
  222. }
  223. func TestTaskSizeConvert(t *testing.T) {
  224. template := convertYaml(t, `
  225. services:
  226. test:
  227. image: nginx
  228. `)
  229. def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  230. assert.Equal(t, def.Cpu, "256")
  231. assert.Equal(t, def.Memory, "512")
  232. template = convertYaml(t, `
  233. services:
  234. test:
  235. image: nginx
  236. deploy:
  237. resources:
  238. limits:
  239. cpus: '0.5'
  240. memory: 2048M
  241. `)
  242. def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  243. assert.Equal(t, def.Cpu, "512")
  244. assert.Equal(t, def.Memory, "2048")
  245. template = convertYaml(t, `
  246. services:
  247. test:
  248. image: nginx
  249. deploy:
  250. resources:
  251. limits:
  252. cpus: '4'
  253. memory: 8192M
  254. `)
  255. def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  256. assert.Equal(t, def.Cpu, "4096")
  257. assert.Equal(t, def.Memory, "8192")
  258. template = convertYaml(t, `
  259. services:
  260. test:
  261. image: nginx
  262. deploy:
  263. resources:
  264. limits:
  265. cpus: '4'
  266. memory: 792Mb
  267. reservations:
  268. generic_resources:
  269. - discrete_resource_spec:
  270. kind: gpus
  271. value: 2
  272. `)
  273. def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  274. assert.Equal(t, def.Cpu, "4000")
  275. assert.Equal(t, def.Memory, "792")
  276. }
  277. func TestTaskSizeConvertFailure(t *testing.T) {
  278. model := loadConfig(t, `
  279. services:
  280. test:
  281. image: nginx
  282. deploy:
  283. resources:
  284. limits:
  285. cpus: '0.5'
  286. memory: 2043248M
  287. `)
  288. backend := &ecsAPIService{}
  289. _, _, err := backend.convert(model)
  290. assert.ErrorContains(t, err, "the resources requested are not supported by ECS/Fargate")
  291. }
  292. func TestLoadBalancerTypeNetwork(t *testing.T) {
  293. template := convertYaml(t, `
  294. services:
  295. test:
  296. image: nginx
  297. ports:
  298. - 80:80
  299. - 88:88
  300. `)
  301. lb := template.Resources["TestLoadBalancer"]
  302. assert.Check(t, lb != nil)
  303. loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
  304. assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumNetwork)
  305. }
  306. func TestServiceMapping(t *testing.T) {
  307. template := convertYaml(t, `
  308. services:
  309. test:
  310. image: "image"
  311. command: "command"
  312. entrypoint: "entrypoint"
  313. environment:
  314. - "FOO=BAR"
  315. cap_add:
  316. - SYS_PTRACE
  317. cap_drop:
  318. - SYSLOG
  319. init: true
  320. user: "user"
  321. working_dir: "working_dir"
  322. `)
  323. def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  324. container := def.ContainerDefinitions[0]
  325. assert.Equal(t, container.Image, "image")
  326. assert.Equal(t, container.Command[0], "command")
  327. assert.Equal(t, container.EntryPoint[0], "entrypoint")
  328. assert.Equal(t, get(container.Environment, "FOO"), "BAR")
  329. assert.Check(t, container.LinuxParameters.InitProcessEnabled)
  330. assert.Equal(t, container.LinuxParameters.Capabilities.Add[0], "SYS_PTRACE")
  331. assert.Equal(t, container.LinuxParameters.Capabilities.Drop[0], "SYSLOG")
  332. assert.Equal(t, container.User, "user")
  333. assert.Equal(t, container.WorkingDirectory, "working_dir")
  334. }
  335. func get(l []ecs.TaskDefinition_KeyValuePair, name string) string {
  336. for _, e := range l {
  337. if e.Name == name {
  338. return e.Value
  339. }
  340. }
  341. return ""
  342. }
  343. func TestResourcesHaveProjectTagSet(t *testing.T) {
  344. template := convertYaml(t, `
  345. services:
  346. test:
  347. image: nginx
  348. ports:
  349. - 80:80
  350. - 88:88
  351. `)
  352. for _, r := range template.Resources {
  353. tags := reflect.Indirect(reflect.ValueOf(r)).FieldByName("Tags")
  354. if !tags.IsValid() {
  355. continue
  356. }
  357. for i := 0; i < tags.Len(); i++ {
  358. k := tags.Index(i).FieldByName("Key").String()
  359. v := tags.Index(i).FieldByName("Value").String()
  360. if k == compose.ProjectTag {
  361. assert.Equal(t, v, "Test")
  362. }
  363. }
  364. }
  365. }
  366. func convertResultAsString(t *testing.T, project *types.Project) string {
  367. backend := &ecsAPIService{}
  368. template, _, err := backend.convert(project)
  369. assert.NilError(t, err)
  370. resultAsJSON, err := marshall(template)
  371. assert.NilError(t, err)
  372. return fmt.Sprintf("%s\n", string(resultAsJSON))
  373. }
  374. func load(t *testing.T, paths ...string) *types.Project {
  375. options := cli.ProjectOptions{
  376. Name: t.Name(),
  377. ConfigPaths: paths,
  378. }
  379. project, err := cli.ProjectFromOptions(&options)
  380. assert.NilError(t, err)
  381. return project
  382. }
  383. func convertYaml(t *testing.T, yaml string) *cloudformation.Template {
  384. project := loadConfig(t, yaml)
  385. backend := &ecsAPIService{}
  386. template, _, err := backend.convert(project)
  387. assert.NilError(t, err)
  388. return template
  389. }
  390. func loadConfig(t *testing.T, yaml string) *types.Project {
  391. dict, err := loader.ParseYAML([]byte(yaml))
  392. assert.NilError(t, err)
  393. model, err := loader.Load(types.ConfigDetails{
  394. ConfigFiles: []types.ConfigFile{
  395. {Config: dict},
  396. },
  397. }, func(options *loader.Options) {
  398. options.Name = "Test"
  399. })
  400. assert.NilError(t, err)
  401. return model
  402. }