cloudformation_test.go 12 KB

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