cloudformation_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  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. "fmt"
  16. "reflect"
  17. "testing"
  18. "github.com/docker/compose-cli/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. template := convertYaml(t, `
  165. services:
  166. test:
  167. image: nginx
  168. ports:
  169. - 80:80
  170. `)
  171. lb := template.Resources["TestLoadBalancer"]
  172. assert.Check(t, lb != nil)
  173. loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
  174. assert.Check(t, len(loadBalancer.Name) <= 32)
  175. assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumApplication)
  176. assert.Check(t, len(loadBalancer.SecurityGroups) > 0)
  177. }
  178. func TestNoLoadBalancerIfNoPortExposed(t *testing.T) {
  179. template := convertYaml(t, `
  180. services:
  181. test:
  182. image: nginx
  183. foo:
  184. image: bar
  185. `)
  186. for _, r := range template.Resources {
  187. assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::TargetGroup")
  188. assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::Listener")
  189. assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::PortPublisher")
  190. }
  191. }
  192. func TestServiceReplicas(t *testing.T) {
  193. template := convertYaml(t, `
  194. services:
  195. test:
  196. image: nginx
  197. deploy:
  198. replicas: 10
  199. `)
  200. s := template.Resources["TestService"]
  201. assert.Check(t, s != nil)
  202. service := *s.(*ecs.Service)
  203. assert.Check(t, service.DesiredCount == 10)
  204. }
  205. func TestTaskSizeConvert(t *testing.T) {
  206. template := convertYaml(t, `
  207. services:
  208. test:
  209. image: nginx
  210. deploy:
  211. resources:
  212. limits:
  213. cpus: '0.5'
  214. memory: 2048M
  215. reservations:
  216. cpus: '0.5'
  217. memory: 2048M
  218. `)
  219. def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  220. assert.Equal(t, def.Cpu, "512")
  221. assert.Equal(t, def.Memory, "2048")
  222. template = convertYaml(t, `
  223. services:
  224. test:
  225. image: nginx
  226. deploy:
  227. resources:
  228. limits:
  229. cpus: '4'
  230. memory: 8192M
  231. reservations:
  232. cpus: '4'
  233. memory: 8192M
  234. `)
  235. def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  236. assert.Equal(t, def.Cpu, "4096")
  237. assert.Equal(t, def.Memory, "8192")
  238. }
  239. func TestTaskSizeConvertFailure(t *testing.T) {
  240. model := loadConfig(t, `
  241. services:
  242. test:
  243. image: nginx
  244. deploy:
  245. resources:
  246. limits:
  247. cpus: '0.5'
  248. memory: 2043248M
  249. `)
  250. backend := &ecsAPIService{}
  251. _, err := backend.convert(model)
  252. assert.ErrorContains(t, err, "the resources requested are not supported by ECS/Fargate")
  253. }
  254. func TestLoadBalancerTypeNetwork(t *testing.T) {
  255. template := convertYaml(t, `
  256. services:
  257. test:
  258. image: nginx
  259. ports:
  260. - 80:80
  261. - 88:88
  262. `)
  263. lb := template.Resources["TestLoadBalancer"]
  264. assert.Check(t, lb != nil)
  265. loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
  266. assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumNetwork)
  267. }
  268. func TestServiceMapping(t *testing.T) {
  269. template := convertYaml(t, `
  270. services:
  271. test:
  272. image: "image"
  273. command: "command"
  274. entrypoint: "entrypoint"
  275. environment:
  276. - "FOO=BAR"
  277. cap_add:
  278. - SYS_PTRACE
  279. cap_drop:
  280. - SYSLOG
  281. init: true
  282. user: "user"
  283. working_dir: "working_dir"
  284. `)
  285. def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  286. container := def.ContainerDefinitions[0]
  287. assert.Equal(t, container.Image, "image")
  288. assert.Equal(t, container.Command[0], "command")
  289. assert.Equal(t, container.EntryPoint[0], "entrypoint")
  290. assert.Equal(t, get(container.Environment, "FOO"), "BAR")
  291. assert.Check(t, container.LinuxParameters.InitProcessEnabled)
  292. assert.Equal(t, container.LinuxParameters.Capabilities.Add[0], "SYS_PTRACE")
  293. assert.Equal(t, container.LinuxParameters.Capabilities.Drop[0], "SYSLOG")
  294. assert.Equal(t, container.User, "user")
  295. assert.Equal(t, container.WorkingDirectory, "working_dir")
  296. }
  297. func get(l []ecs.TaskDefinition_KeyValuePair, name string) string {
  298. for _, e := range l {
  299. if e.Name == name {
  300. return e.Value
  301. }
  302. }
  303. return ""
  304. }
  305. func TestResourcesHaveProjectTagSet(t *testing.T) {
  306. template := convertYaml(t, `
  307. services:
  308. test:
  309. image: nginx
  310. ports:
  311. - 80:80
  312. - 88:88
  313. `)
  314. for _, r := range template.Resources {
  315. tags := reflect.Indirect(reflect.ValueOf(r)).FieldByName("Tags")
  316. if !tags.IsValid() {
  317. continue
  318. }
  319. for i := 0; i < tags.Len(); i++ {
  320. k := tags.Index(i).FieldByName("Key").String()
  321. v := tags.Index(i).FieldByName("Value").String()
  322. if k == compose.ProjectTag {
  323. assert.Equal(t, v, "Test")
  324. }
  325. }
  326. }
  327. }
  328. func convertResultAsString(t *testing.T, project *types.Project) string {
  329. backend := &ecsAPIService{}
  330. template, err := backend.convert(project)
  331. assert.NilError(t, err)
  332. resultAsJSON, err := marshall(template)
  333. assert.NilError(t, err)
  334. return fmt.Sprintf("%s\n", string(resultAsJSON))
  335. }
  336. func load(t *testing.T, paths ...string) *types.Project {
  337. options := cli.ProjectOptions{
  338. Name: t.Name(),
  339. ConfigPaths: paths,
  340. }
  341. project, err := cli.ProjectFromOptions(&options)
  342. assert.NilError(t, err)
  343. return project
  344. }
  345. func convertYaml(t *testing.T, yaml string) *cloudformation.Template {
  346. project := loadConfig(t, yaml)
  347. backend := &ecsAPIService{}
  348. template, err := backend.convert(project)
  349. assert.NilError(t, err)
  350. return template
  351. }
  352. func loadConfig(t *testing.T, yaml string) *types.Project {
  353. dict, err := loader.ParseYAML([]byte(yaml))
  354. assert.NilError(t, err)
  355. model, err := loader.Load(types.ConfigDetails{
  356. ConfigFiles: []types.ConfigFile{
  357. {Config: dict},
  358. },
  359. }, func(options *loader.Options) {
  360. options.Name = "Test"
  361. })
  362. assert.NilError(t, err)
  363. return model
  364. }