cloudformation_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  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. "context"
  16. "fmt"
  17. "io/ioutil"
  18. "reflect"
  19. "testing"
  20. "github.com/awslabs/goformation/v4/cloudformation/efs"
  21. "github.com/golang/mock/gomock"
  22. "github.com/docker/compose-cli/api/compose"
  23. "github.com/aws/aws-sdk-go/service/elbv2"
  24. "github.com/awslabs/goformation/v4/cloudformation"
  25. "github.com/awslabs/goformation/v4/cloudformation/ec2"
  26. "github.com/awslabs/goformation/v4/cloudformation/ecs"
  27. "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2"
  28. "github.com/awslabs/goformation/v4/cloudformation/iam"
  29. "github.com/awslabs/goformation/v4/cloudformation/logs"
  30. "github.com/compose-spec/compose-go/loader"
  31. "github.com/compose-spec/compose-go/types"
  32. "gotest.tools/v3/assert"
  33. "gotest.tools/v3/golden"
  34. )
  35. func TestSimpleConvert(t *testing.T) {
  36. bytes, err := ioutil.ReadFile("testdata/input/simple-single-service.yaml")
  37. assert.NilError(t, err)
  38. template := convertYaml(t, string(bytes), useDefaultVPC)
  39. resultAsJSON, err := marshall(template)
  40. assert.NilError(t, err)
  41. result := fmt.Sprintf("%s\n", string(resultAsJSON))
  42. expected := "simple/simple-cloudformation-conversion.golden"
  43. golden.Assert(t, result, expected)
  44. }
  45. func TestLogging(t *testing.T) {
  46. template := convertYaml(t, `
  47. services:
  48. foo:
  49. image: hello_world
  50. logging:
  51. options:
  52. awslogs-datetime-pattern: "FOO"
  53. x-aws-logs_retention: 10
  54. `, useDefaultVPC)
  55. def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition)
  56. logging := getMainContainer(def, t).LogConfiguration
  57. if logging != nil {
  58. assert.Equal(t, logging.Options["awslogs-datetime-pattern"], "FOO")
  59. } else {
  60. t.Fatal("Logging not configured")
  61. }
  62. logGroup := template.Resources["LogGroup"].(*logs.LogGroup)
  63. assert.Equal(t, logGroup.RetentionInDays, 10)
  64. }
  65. func TestEnvFile(t *testing.T) {
  66. template := convertYaml(t, `
  67. services:
  68. foo:
  69. image: hello_world
  70. env_file:
  71. - testdata/input/envfile
  72. `, useDefaultVPC)
  73. def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition)
  74. env := getMainContainer(def, t).Environment
  75. var found bool
  76. for _, pair := range env {
  77. if pair.Name == "FOO" {
  78. assert.Equal(t, pair.Value, "BAR")
  79. found = true
  80. }
  81. }
  82. assert.Check(t, found, "environment variable FOO not set")
  83. }
  84. func TestEnvFileAndEnv(t *testing.T) {
  85. template := convertYaml(t, `
  86. services:
  87. foo:
  88. image: hello_world
  89. env_file:
  90. - testdata/input/envfile
  91. environment:
  92. - "FOO=ZOT"
  93. `, useDefaultVPC)
  94. def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition)
  95. env := getMainContainer(def, t).Environment
  96. var found bool
  97. for _, pair := range env {
  98. if pair.Name == "FOO" {
  99. assert.Equal(t, pair.Value, "ZOT")
  100. found = true
  101. }
  102. }
  103. assert.Check(t, found, "environment variable FOO not set")
  104. }
  105. func TestRollingUpdateLimits(t *testing.T) {
  106. template := convertYaml(t, `
  107. services:
  108. foo:
  109. image: hello_world
  110. deploy:
  111. replicas: 4
  112. update_config:
  113. parallelism: 2
  114. `, useDefaultVPC)
  115. service := template.Resources["FooService"].(*ecs.Service)
  116. assert.Check(t, service.DeploymentConfiguration.MaximumPercent == 150)
  117. assert.Check(t, service.DeploymentConfiguration.MinimumHealthyPercent == 50)
  118. }
  119. func TestRollingUpdateExtension(t *testing.T) {
  120. template := convertYaml(t, `
  121. services:
  122. foo:
  123. image: hello_world
  124. deploy:
  125. update_config:
  126. x-aws-min_percent: 25
  127. x-aws-max_percent: 125
  128. `, useDefaultVPC)
  129. service := template.Resources["FooService"].(*ecs.Service)
  130. assert.Check(t, service.DeploymentConfiguration.MaximumPercent == 125)
  131. assert.Check(t, service.DeploymentConfiguration.MinimumHealthyPercent == 25)
  132. }
  133. func TestRolePolicy(t *testing.T) {
  134. template := convertYaml(t, `
  135. services:
  136. foo:
  137. image: hello_world
  138. x-aws-pull_credentials: "secret"
  139. `, useDefaultVPC)
  140. x := template.Resources["FooTaskExecutionRole"]
  141. assert.Check(t, x != nil)
  142. role := *(x.(*iam.Role))
  143. assert.Check(t, role.ManagedPolicyArns[0] == ecsTaskExecutionPolicy)
  144. assert.Check(t, role.ManagedPolicyArns[1] == ecrReadOnlyPolicy)
  145. // We expect an extra policy has been created for x-aws-pull_credentials
  146. assert.Check(t, len(role.Policies) == 1)
  147. policy := role.Policies[0].PolicyDocument.(*PolicyDocument)
  148. expected := []string{"secretsmanager:GetSecretValue", "ssm:GetParameters", "kms:Decrypt"}
  149. assert.DeepEqual(t, expected, policy.Statement[0].Action)
  150. assert.DeepEqual(t, []string{"secret"}, policy.Statement[0].Resource)
  151. }
  152. func TestMapNetworksToSecurityGroups(t *testing.T) {
  153. template := convertYaml(t, `
  154. services:
  155. test:
  156. image: hello_world
  157. networks:
  158. - front-tier
  159. - back-tier
  160. networks:
  161. front-tier:
  162. name: public
  163. back-tier:
  164. internal: true
  165. `, useDefaultVPC)
  166. assert.Check(t, template.Resources["FronttierNetwork"] != nil)
  167. assert.Check(t, template.Resources["BacktierNetwork"] != nil)
  168. assert.Check(t, template.Resources["BacktierNetworkIngress"] != nil)
  169. i := template.Resources["FronttierNetworkIngress"]
  170. assert.Check(t, i != nil)
  171. ingress := *i.(*ec2.SecurityGroupIngress)
  172. assert.Check(t, ingress.SourceSecurityGroupId == cloudformation.Ref("FronttierNetwork"))
  173. }
  174. func TestLoadBalancerTypeApplication(t *testing.T) {
  175. cases := []string{
  176. `services:
  177. test:
  178. image: nginx
  179. ports:
  180. - 80:80
  181. `,
  182. `services:
  183. test:
  184. image: nginx
  185. ports:
  186. - target: 8080
  187. x-aws-protocol: http
  188. `,
  189. }
  190. for _, y := range cases {
  191. template := convertYaml(t, y, useDefaultVPC)
  192. lb := template.Resources["LoadBalancer"]
  193. assert.Check(t, lb != nil)
  194. loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
  195. assert.Check(t, len(loadBalancer.Name) <= 32)
  196. assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumApplication)
  197. assert.Check(t, len(loadBalancer.SecurityGroups) > 0)
  198. }
  199. }
  200. func TestNoLoadBalancerIfNoPortExposed(t *testing.T) {
  201. template := convertYaml(t, `
  202. services:
  203. test:
  204. image: nginx
  205. foo:
  206. image: bar
  207. `, useDefaultVPC)
  208. for _, r := range template.Resources {
  209. assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::TargetGroup")
  210. assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::Listener")
  211. assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::PortPublisher")
  212. }
  213. }
  214. func TestServiceReplicas(t *testing.T) {
  215. template := convertYaml(t, `
  216. services:
  217. test:
  218. image: nginx
  219. deploy:
  220. replicas: 10
  221. `, useDefaultVPC)
  222. s := template.Resources["TestService"]
  223. assert.Check(t, s != nil)
  224. service := *s.(*ecs.Service)
  225. assert.Check(t, service.DesiredCount == 10)
  226. }
  227. func TestTaskSizeConvert(t *testing.T) {
  228. template := convertYaml(t, `
  229. services:
  230. test:
  231. image: nginx
  232. `, useDefaultVPC)
  233. def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  234. assert.Equal(t, def.Cpu, "256")
  235. assert.Equal(t, def.Memory, "512")
  236. template = convertYaml(t, `
  237. services:
  238. test:
  239. image: nginx
  240. deploy:
  241. resources:
  242. limits:
  243. cpus: '0.5'
  244. memory: 2048M
  245. `, useDefaultVPC)
  246. def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  247. assert.Equal(t, def.Cpu, "512")
  248. assert.Equal(t, def.Memory, "2048")
  249. template = convertYaml(t, `
  250. services:
  251. test:
  252. image: nginx
  253. deploy:
  254. resources:
  255. limits:
  256. cpus: '4'
  257. memory: 8192M
  258. `, useDefaultVPC)
  259. def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  260. assert.Equal(t, def.Cpu, "4096")
  261. assert.Equal(t, def.Memory, "8192")
  262. template = convertYaml(t, `
  263. services:
  264. test:
  265. image: nginx
  266. deploy:
  267. resources:
  268. limits:
  269. cpus: '4'
  270. memory: 792Mb
  271. reservations:
  272. generic_resources:
  273. - discrete_resource_spec:
  274. kind: gpus
  275. value: 2
  276. `, useDefaultVPC, useGPU)
  277. def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  278. assert.Equal(t, def.Cpu, "4000")
  279. assert.Equal(t, def.Memory, "792")
  280. template = convertYaml(t, `
  281. services:
  282. test:
  283. image: nginx
  284. deploy:
  285. resources:
  286. reservations:
  287. generic_resources:
  288. - discrete_resource_spec:
  289. kind: gpus
  290. value: 2
  291. `, useDefaultVPC, useGPU)
  292. def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  293. assert.Equal(t, def.Cpu, "")
  294. assert.Equal(t, def.Memory, "")
  295. }
  296. func TestLoadBalancerTypeNetwork(t *testing.T) {
  297. template := convertYaml(t, `
  298. services:
  299. test:
  300. image: nginx
  301. ports:
  302. - 80:80
  303. - 88:88
  304. `, useDefaultVPC)
  305. lb := template.Resources["LoadBalancer"]
  306. assert.Check(t, lb != nil)
  307. loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
  308. assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumNetwork)
  309. }
  310. func TestUseExternalNetwork(t *testing.T) {
  311. template := convertYaml(t, `
  312. services:
  313. test:
  314. image: nginx
  315. networks:
  316. default:
  317. external: true
  318. name: sg-123abc
  319. `, useDefaultVPC, func(m *MockAPIMockRecorder) {
  320. m.SecurityGroupExists(gomock.Any(), "sg-123abc").Return(true, nil)
  321. })
  322. assert.Check(t, template.Resources["DefaultNetwork"] == nil)
  323. assert.Check(t, template.Resources["DefaultNetworkIngress"] == nil)
  324. s := template.Resources["TestService"].(*ecs.Service)
  325. assert.Check(t, s != nil)
  326. assert.Check(t, s.NetworkConfiguration.AwsvpcConfiguration.SecurityGroups[0] == "sg-123abc") //nolint:staticcheck
  327. }
  328. func testVolume(t *testing.T, yaml string, fn ...func(m *MockAPIMockRecorder)) {
  329. template := convertYaml(t, yaml, fn...)
  330. s := template.Resources["DbdataNFSMountTargetOnSubnet1"].(*efs.MountTarget)
  331. assert.Check(t, s != nil)
  332. assert.Equal(t, s.FileSystemId, "fs-123abc") //nolint:staticcheck
  333. s = template.Resources["DbdataNFSMountTargetOnSubnet2"].(*efs.MountTarget)
  334. assert.Check(t, s != nil)
  335. assert.Equal(t, s.FileSystemId, "fs-123abc") //nolint:staticcheck
  336. }
  337. func TestUseExternalVolume(t *testing.T) {
  338. testVolume(t, `
  339. services:
  340. test:
  341. image: nginx
  342. volumes:
  343. db-data:
  344. external: true
  345. name: fs-123abc
  346. `, useDefaultVPC, func(m *MockAPIMockRecorder) {
  347. m.FileSystemExists(gomock.Any(), "fs-123abc").Return(true, nil)
  348. })
  349. }
  350. func TestCreateVolume(t *testing.T) {
  351. testVolume(t, `
  352. services:
  353. test:
  354. image: nginx
  355. volumes:
  356. db-data: {}
  357. `, useDefaultVPC, func(m *MockAPIMockRecorder) {
  358. m.FindFileSystem(gomock.Any(), map[string]string{
  359. compose.ProjectTag: t.Name(),
  360. compose.VolumeTag: "db-data",
  361. }).Return("", nil)
  362. m.CreateFileSystem(gomock.Any(), map[string]string{
  363. compose.ProjectTag: t.Name(),
  364. compose.VolumeTag: "db-data",
  365. "Name": fmt.Sprintf("%s_%s", t.Name(), "db-data"),
  366. }).Return("fs-123abc", nil)
  367. })
  368. }
  369. func TestReusePreviousVolume(t *testing.T) {
  370. testVolume(t, `
  371. services:
  372. test:
  373. image: nginx
  374. volumes:
  375. db-data: {}
  376. `, useDefaultVPC, func(m *MockAPIMockRecorder) {
  377. m.FindFileSystem(gomock.Any(), map[string]string{
  378. compose.ProjectTag: t.Name(),
  379. compose.VolumeTag: "db-data",
  380. }).Return("fs-123abc", nil)
  381. })
  382. }
  383. func TestServiceMapping(t *testing.T) {
  384. template := convertYaml(t, `
  385. services:
  386. test:
  387. image: "image"
  388. command: "command"
  389. entrypoint: "entrypoint"
  390. environment:
  391. - "FOO=BAR"
  392. cap_add:
  393. - SYS_PTRACE
  394. cap_drop:
  395. - SYSLOG
  396. init: true
  397. user: "user"
  398. working_dir: "working_dir"
  399. `, useDefaultVPC)
  400. def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  401. container := getMainContainer(def, t)
  402. assert.Equal(t, container.Image, "image")
  403. assert.Equal(t, container.Command[0], "command")
  404. assert.Equal(t, container.EntryPoint[0], "entrypoint")
  405. assert.Equal(t, get(container.Environment, "FOO"), "BAR")
  406. assert.Check(t, container.LinuxParameters.InitProcessEnabled)
  407. assert.Equal(t, container.LinuxParameters.Capabilities.Add[0], "SYS_PTRACE")
  408. assert.Equal(t, container.LinuxParameters.Capabilities.Drop[0], "SYSLOG")
  409. assert.Equal(t, container.User, "user")
  410. assert.Equal(t, container.WorkingDirectory, "working_dir")
  411. }
  412. func get(l []ecs.TaskDefinition_KeyValuePair, name string) string {
  413. for _, e := range l {
  414. if e.Name == name {
  415. return e.Value
  416. }
  417. }
  418. return ""
  419. }
  420. func TestResourcesHaveProjectTagSet(t *testing.T) {
  421. template := convertYaml(t, `
  422. services:
  423. test:
  424. image: nginx
  425. ports:
  426. - 80:80
  427. - 88:88
  428. `, useDefaultVPC)
  429. for _, r := range template.Resources {
  430. tags := reflect.Indirect(reflect.ValueOf(r)).FieldByName("Tags")
  431. if !tags.IsValid() {
  432. continue
  433. }
  434. for i := 0; i < tags.Len(); i++ {
  435. k := tags.Index(i).FieldByName("Key").String()
  436. v := tags.Index(i).FieldByName("Value").String()
  437. if k == compose.ProjectTag {
  438. assert.Equal(t, v, t.Name())
  439. }
  440. }
  441. }
  442. }
  443. func convertYaml(t *testing.T, yaml string, fn ...func(m *MockAPIMockRecorder)) *cloudformation.Template {
  444. project := loadConfig(t, yaml)
  445. ctrl := gomock.NewController(t)
  446. defer ctrl.Finish()
  447. m := NewMockAPI(ctrl)
  448. for _, f := range fn {
  449. f(m.EXPECT())
  450. }
  451. backend := &ecsAPIService{
  452. aws: m,
  453. }
  454. template, err := backend.convert(context.TODO(), project)
  455. assert.NilError(t, err)
  456. return template
  457. }
  458. func loadConfig(t *testing.T, yaml string) *types.Project {
  459. dict, err := loader.ParseYAML([]byte(yaml))
  460. assert.NilError(t, err)
  461. model, err := loader.Load(types.ConfigDetails{
  462. ConfigFiles: []types.ConfigFile{
  463. {Config: dict},
  464. },
  465. }, func(options *loader.Options) {
  466. options.Name = t.Name()
  467. })
  468. assert.NilError(t, err)
  469. return model
  470. }
  471. func getMainContainer(def *ecs.TaskDefinition, t *testing.T) ecs.TaskDefinition_ContainerDefinition {
  472. for _, c := range def.ContainerDefinitions {
  473. if c.Essential {
  474. return c
  475. }
  476. }
  477. t.Fail()
  478. return def.ContainerDefinitions[0]
  479. }
  480. func useDefaultVPC(m *MockAPIMockRecorder) {
  481. m.GetDefaultVPC(gomock.Any()).Return("vpc-123", nil)
  482. m.GetSubNets(gomock.Any(), "vpc-123").Return([]string{"subnet1", "subnet2"}, nil)
  483. }
  484. func useGPU(m *MockAPIMockRecorder) {
  485. m.GetParameter(gomock.Any(), gomock.Any()).Return("", nil)
  486. }