cloudformation_test.go 12 KB

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