cloudformation_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  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["FronttierNetwork"] != nil)
  156. assert.Check(t, template.Resources["BacktierNetwork"] != nil)
  157. assert.Check(t, template.Resources["BacktierNetworkIngress"] != nil)
  158. i := template.Resources["FronttierNetworkIngress"]
  159. assert.Check(t, i != nil)
  160. ingress := *i.(*ec2.SecurityGroupIngress)
  161. assert.Check(t, ingress.SourceSecurityGroupId == cloudformation.Ref("FronttierNetwork"))
  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. x-aws-protocol: http
  177. `,
  178. }
  179. for _, y := range cases {
  180. template := convertYaml(t, y)
  181. lb := template.Resources["LoadBalancer"]
  182. assert.Check(t, lb != nil)
  183. loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
  184. assert.Check(t, len(loadBalancer.Name) <= 32)
  185. assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumApplication)
  186. assert.Check(t, len(loadBalancer.SecurityGroups) > 0)
  187. }
  188. }
  189. func TestNoLoadBalancerIfNoPortExposed(t *testing.T) {
  190. template := convertYaml(t, `
  191. services:
  192. test:
  193. image: nginx
  194. foo:
  195. image: bar
  196. `)
  197. for _, r := range template.Resources {
  198. assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::TargetGroup")
  199. assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::Listener")
  200. assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::PortPublisher")
  201. }
  202. }
  203. func TestServiceReplicas(t *testing.T) {
  204. template := convertYaml(t, `
  205. services:
  206. test:
  207. image: nginx
  208. deploy:
  209. replicas: 10
  210. `)
  211. s := template.Resources["TestService"]
  212. assert.Check(t, s != nil)
  213. service := *s.(*ecs.Service)
  214. assert.Check(t, service.DesiredCount == 10)
  215. }
  216. func TestTaskSizeConvert(t *testing.T) {
  217. template := convertYaml(t, `
  218. services:
  219. test:
  220. image: nginx
  221. `)
  222. def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  223. assert.Equal(t, def.Cpu, "256")
  224. assert.Equal(t, def.Memory, "512")
  225. template = convertYaml(t, `
  226. services:
  227. test:
  228. image: nginx
  229. deploy:
  230. resources:
  231. limits:
  232. cpus: '0.5'
  233. memory: 2048M
  234. `)
  235. def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  236. assert.Equal(t, def.Cpu, "512")
  237. assert.Equal(t, def.Memory, "2048")
  238. template = convertYaml(t, `
  239. services:
  240. test:
  241. image: nginx
  242. deploy:
  243. resources:
  244. limits:
  245. cpus: '4'
  246. memory: 8192M
  247. `)
  248. def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  249. assert.Equal(t, def.Cpu, "4096")
  250. assert.Equal(t, def.Memory, "8192")
  251. template = convertYaml(t, `
  252. services:
  253. test:
  254. image: nginx
  255. deploy:
  256. resources:
  257. limits:
  258. cpus: '4'
  259. memory: 792Mb
  260. reservations:
  261. generic_resources:
  262. - discrete_resource_spec:
  263. kind: gpus
  264. value: 2
  265. `)
  266. def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  267. assert.Equal(t, def.Cpu, "4000")
  268. assert.Equal(t, def.Memory, "792")
  269. template = convertYaml(t, `
  270. services:
  271. test:
  272. image: nginx
  273. deploy:
  274. resources:
  275. reservations:
  276. generic_resources:
  277. - discrete_resource_spec:
  278. kind: gpus
  279. value: 2
  280. `)
  281. def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  282. assert.Equal(t, def.Cpu, "")
  283. assert.Equal(t, def.Memory, "")
  284. }
  285. func TestTaskSizeConvertFailure(t *testing.T) {
  286. model := loadConfig(t, `
  287. services:
  288. test:
  289. image: nginx
  290. deploy:
  291. resources:
  292. limits:
  293. cpus: '0.5'
  294. memory: 2043248M
  295. `)
  296. backend := &ecsAPIService{}
  297. _, err := backend.convert(model, awsResources{})
  298. assert.ErrorContains(t, err, "the resources requested are not supported by ECS/Fargate")
  299. }
  300. func TestLoadBalancerTypeNetwork(t *testing.T) {
  301. template := convertYaml(t, `
  302. services:
  303. test:
  304. image: nginx
  305. ports:
  306. - 80:80
  307. - 88:88
  308. `)
  309. lb := template.Resources["LoadBalancer"]
  310. assert.Check(t, lb != nil)
  311. loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
  312. assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumNetwork)
  313. }
  314. func TestServiceMapping(t *testing.T) {
  315. template := convertYaml(t, `
  316. services:
  317. test:
  318. image: "image"
  319. command: "command"
  320. entrypoint: "entrypoint"
  321. environment:
  322. - "FOO=BAR"
  323. cap_add:
  324. - SYS_PTRACE
  325. cap_drop:
  326. - SYSLOG
  327. init: true
  328. user: "user"
  329. working_dir: "working_dir"
  330. `)
  331. def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  332. container := def.ContainerDefinitions[0]
  333. assert.Equal(t, container.Image, "image")
  334. assert.Equal(t, container.Command[0], "command")
  335. assert.Equal(t, container.EntryPoint[0], "entrypoint")
  336. assert.Equal(t, get(container.Environment, "FOO"), "BAR")
  337. assert.Check(t, container.LinuxParameters.InitProcessEnabled)
  338. assert.Equal(t, container.LinuxParameters.Capabilities.Add[0], "SYS_PTRACE")
  339. assert.Equal(t, container.LinuxParameters.Capabilities.Drop[0], "SYSLOG")
  340. assert.Equal(t, container.User, "user")
  341. assert.Equal(t, container.WorkingDirectory, "working_dir")
  342. }
  343. func get(l []ecs.TaskDefinition_KeyValuePair, name string) string {
  344. for _, e := range l {
  345. if e.Name == name {
  346. return e.Value
  347. }
  348. }
  349. return ""
  350. }
  351. func TestResourcesHaveProjectTagSet(t *testing.T) {
  352. template := convertYaml(t, `
  353. services:
  354. test:
  355. image: nginx
  356. ports:
  357. - 80:80
  358. - 88:88
  359. `)
  360. for _, r := range template.Resources {
  361. tags := reflect.Indirect(reflect.ValueOf(r)).FieldByName("Tags")
  362. if !tags.IsValid() {
  363. continue
  364. }
  365. for i := 0; i < tags.Len(); i++ {
  366. k := tags.Index(i).FieldByName("Key").String()
  367. v := tags.Index(i).FieldByName("Value").String()
  368. if k == compose.ProjectTag {
  369. assert.Equal(t, v, "Test")
  370. }
  371. }
  372. }
  373. }
  374. func convertResultAsString(t *testing.T, project *types.Project) string {
  375. backend := &ecsAPIService{}
  376. template, err := backend.convert(project, awsResources{
  377. vpc: "vpcID",
  378. subnets: []string{"subnet1", "subnet2"},
  379. })
  380. assert.NilError(t, err)
  381. resultAsJSON, err := marshall(template)
  382. assert.NilError(t, err)
  383. return fmt.Sprintf("%s\n", string(resultAsJSON))
  384. }
  385. func load(t *testing.T, paths ...string) *types.Project {
  386. options := cli.ProjectOptions{
  387. Name: t.Name(),
  388. ConfigPaths: paths,
  389. }
  390. project, err := cli.ProjectFromOptions(&options)
  391. assert.NilError(t, err)
  392. return project
  393. }
  394. func convertYaml(t *testing.T, yaml string) *cloudformation.Template {
  395. project := loadConfig(t, yaml)
  396. backend := &ecsAPIService{}
  397. template, err := backend.convert(project, awsResources{})
  398. assert.NilError(t, err)
  399. return template
  400. }
  401. func loadConfig(t *testing.T, yaml string) *types.Project {
  402. dict, err := loader.ParseYAML([]byte(yaml))
  403. assert.NilError(t, err)
  404. model, err := loader.Load(types.ConfigDetails{
  405. ConfigFiles: []types.ConfigFile{
  406. {Config: dict},
  407. },
  408. }, func(options *loader.Options) {
  409. options.Name = "Test"
  410. })
  411. assert.NilError(t, err)
  412. return model
  413. }