cloudformation_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  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/docker/compose-cli/api/compose"
  21. "github.com/aws/aws-sdk-go/service/elbv2"
  22. "github.com/awslabs/goformation/v4/cloudformation"
  23. "github.com/awslabs/goformation/v4/cloudformation/ec2"
  24. "github.com/awslabs/goformation/v4/cloudformation/ecs"
  25. "github.com/awslabs/goformation/v4/cloudformation/efs"
  26. "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2"
  27. "github.com/awslabs/goformation/v4/cloudformation/iam"
  28. "github.com/awslabs/goformation/v4/cloudformation/logs"
  29. "github.com/compose-spec/compose-go/loader"
  30. "github.com/compose-spec/compose-go/types"
  31. "github.com/golang/mock/gomock"
  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 TestUseExternalVolume(t *testing.T) {
  329. template := convertYaml(t, `
  330. services:
  331. test:
  332. image: nginx
  333. volumes:
  334. db-data:
  335. external: true
  336. name: fs-123abc
  337. `, useDefaultVPC, func(m *MockAPIMockRecorder) {
  338. m.ResolveFileSystem(gomock.Any(), "fs-123abc").Return(existingAWSResource{id: "fs-123abc"}, nil)
  339. })
  340. s := template.Resources["DbdataNFSMountTargetOnSubnet1"].(*efs.MountTarget)
  341. assert.Check(t, s != nil)
  342. assert.Equal(t, s.FileSystemId, "fs-123abc") //nolint:staticcheck
  343. s = template.Resources["DbdataNFSMountTargetOnSubnet2"].(*efs.MountTarget)
  344. assert.Check(t, s != nil)
  345. assert.Equal(t, s.FileSystemId, "fs-123abc") //nolint:staticcheck
  346. }
  347. func TestCreateVolume(t *testing.T) {
  348. template := convertYaml(t, `
  349. services:
  350. test:
  351. image: nginx
  352. volumes:
  353. db-data:
  354. driver_opts:
  355. backup_policy: ENABLED
  356. lifecycle_policy: AFTER_30_DAYS
  357. performance_mode: maxIO
  358. throughput_mode: provisioned
  359. provisioned_throughput: 1024
  360. `, useDefaultVPC, func(m *MockAPIMockRecorder) {
  361. m.FindFileSystem(gomock.Any(), map[string]string{
  362. compose.ProjectTag: t.Name(),
  363. compose.VolumeTag: "db-data",
  364. }).Return(nil, nil)
  365. })
  366. n := volumeResourceName("db-data")
  367. f := template.Resources[n].(*efs.FileSystem)
  368. assert.Check(t, f != nil)
  369. assert.Equal(t, f.BackupPolicy.Status, "ENABLED") //nolint:staticcheck
  370. assert.Equal(t, f.LifecyclePolicies[0].TransitionToIA, "AFTER_30_DAYS") //nolint:staticcheck
  371. assert.Equal(t, f.PerformanceMode, "maxIO") //nolint:staticcheck
  372. assert.Equal(t, f.ThroughputMode, "provisioned") //nolint:staticcheck
  373. assert.Equal(t, f.ProvisionedThroughputInMibps, float64(1024)) //nolint:staticcheck
  374. s := template.Resources["DbdataNFSMountTargetOnSubnet1"].(*efs.MountTarget)
  375. assert.Check(t, s != nil)
  376. assert.Equal(t, s.FileSystemId, cloudformation.Ref(n)) //nolint:staticcheck
  377. }
  378. func TestCreateAccessPoint(t *testing.T) {
  379. template := convertYaml(t, `
  380. services:
  381. test:
  382. image: nginx
  383. volumes:
  384. db-data:
  385. driver_opts:
  386. uid: 1002
  387. gid: 1002
  388. `, useDefaultVPC, func(m *MockAPIMockRecorder) {
  389. m.FindFileSystem(gomock.Any(), gomock.Any()).Return(nil, nil)
  390. })
  391. a := template.Resources["DbdataAccessPoint"].(*efs.AccessPoint)
  392. assert.Check(t, a != nil)
  393. assert.Equal(t, a.PosixUser.Uid, "1002") //nolint:staticcheck
  394. assert.Equal(t, a.PosixUser.Gid, "1002") //nolint:staticcheck
  395. }
  396. func TestReusePreviousVolume(t *testing.T) {
  397. template := convertYaml(t, `
  398. services:
  399. test:
  400. image: nginx
  401. volumes:
  402. db-data: {}
  403. `, useDefaultVPC, func(m *MockAPIMockRecorder) {
  404. m.FindFileSystem(gomock.Any(), map[string]string{
  405. compose.ProjectTag: t.Name(),
  406. compose.VolumeTag: "db-data",
  407. }).Return(existingAWSResource{id: "fs-123abc"}, nil)
  408. })
  409. s := template.Resources["DbdataNFSMountTargetOnSubnet1"].(*efs.MountTarget)
  410. assert.Check(t, s != nil)
  411. assert.Equal(t, s.FileSystemId, "fs-123abc") //nolint:staticcheck
  412. }
  413. func TestServiceMapping(t *testing.T) {
  414. template := convertYaml(t, `
  415. services:
  416. test:
  417. image: "image"
  418. command: "command"
  419. entrypoint: "entrypoint"
  420. environment:
  421. - "FOO=BAR"
  422. cap_add:
  423. - SYS_PTRACE
  424. cap_drop:
  425. - SYSLOG
  426. init: true
  427. user: "user"
  428. working_dir: "working_dir"
  429. `, useDefaultVPC)
  430. def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
  431. container := getMainContainer(def, t)
  432. assert.Equal(t, container.Image, "image")
  433. assert.Equal(t, container.Command[0], "command")
  434. assert.Equal(t, container.EntryPoint[0], "entrypoint")
  435. assert.Equal(t, get(container.Environment, "FOO"), "BAR")
  436. assert.Check(t, container.LinuxParameters.InitProcessEnabled)
  437. assert.Equal(t, container.LinuxParameters.Capabilities.Add[0], "SYS_PTRACE")
  438. assert.Equal(t, container.LinuxParameters.Capabilities.Drop[0], "SYSLOG")
  439. assert.Equal(t, container.User, "user")
  440. assert.Equal(t, container.WorkingDirectory, "working_dir")
  441. }
  442. func get(l []ecs.TaskDefinition_KeyValuePair, name string) string {
  443. for _, e := range l {
  444. if e.Name == name {
  445. return e.Value
  446. }
  447. }
  448. return ""
  449. }
  450. func TestResourcesHaveProjectTagSet(t *testing.T) {
  451. template := convertYaml(t, `
  452. services:
  453. test:
  454. image: nginx
  455. ports:
  456. - 80:80
  457. - 88:88
  458. `, useDefaultVPC)
  459. for _, r := range template.Resources {
  460. tags := reflect.Indirect(reflect.ValueOf(r)).FieldByName("Tags")
  461. if !tags.IsValid() {
  462. continue
  463. }
  464. for i := 0; i < tags.Len(); i++ {
  465. k := tags.Index(i).FieldByName("Key").String()
  466. v := tags.Index(i).FieldByName("Value").String()
  467. if k == compose.ProjectTag {
  468. assert.Equal(t, v, t.Name())
  469. }
  470. }
  471. }
  472. }
  473. func TestTemplateMetadata(t *testing.T) {
  474. template := convertYaml(t, `
  475. x-aws-cluster: "arn:aws:ecs:region:account:cluster/name"
  476. services:
  477. test:
  478. image: nginx
  479. `, useDefaultVPC, func(m *MockAPIMockRecorder) {
  480. m.ResolveCluster(gomock.Any(), "arn:aws:ecs:region:account:cluster/name").Return(existingAWSResource{
  481. arn: "arn:aws:ecs:region:account:cluster/name",
  482. id: "name",
  483. }, nil)
  484. })
  485. assert.Equal(t, template.Metadata["Cluster"], "arn:aws:ecs:region:account:cluster/name")
  486. }
  487. func convertYaml(t *testing.T, yaml string, fn ...func(m *MockAPIMockRecorder)) *cloudformation.Template {
  488. project := loadConfig(t, yaml)
  489. ctrl := gomock.NewController(t)
  490. defer ctrl.Finish()
  491. m := NewMockAPI(ctrl)
  492. for _, f := range fn {
  493. f(m.EXPECT())
  494. }
  495. backend := &ecsAPIService{
  496. aws: m,
  497. }
  498. template, err := backend.convert(context.TODO(), project)
  499. assert.NilError(t, err)
  500. return template
  501. }
  502. func loadConfig(t *testing.T, yaml string) *types.Project {
  503. dict, err := loader.ParseYAML([]byte(yaml))
  504. assert.NilError(t, err)
  505. model, err := loader.Load(types.ConfigDetails{
  506. ConfigFiles: []types.ConfigFile{
  507. {Config: dict},
  508. },
  509. }, func(options *loader.Options) {
  510. options.Name = t.Name()
  511. })
  512. assert.NilError(t, err)
  513. return model
  514. }
  515. func getMainContainer(def *ecs.TaskDefinition, t *testing.T) ecs.TaskDefinition_ContainerDefinition {
  516. for _, c := range def.ContainerDefinitions {
  517. if c.Essential {
  518. return c
  519. }
  520. }
  521. t.Fail()
  522. return def.ContainerDefinitions[0]
  523. }
  524. func useDefaultVPC(m *MockAPIMockRecorder) {
  525. m.GetDefaultVPC(gomock.Any()).Return("vpc-123", nil)
  526. m.GetSubNets(gomock.Any(), "vpc-123").Return([]awsResource{
  527. existingAWSResource{id: "subnet1"},
  528. existingAWSResource{id: "subnet2"},
  529. }, nil)
  530. }
  531. func useGPU(m *MockAPIMockRecorder) {
  532. m.GetParameter(gomock.Any(), gomock.Any()).Return("", nil)
  533. }