| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533 |
- /*
- Copyright 2020 Docker Compose CLI authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package ecs
- import (
- "context"
- "fmt"
- "io/ioutil"
- "reflect"
- "testing"
- "github.com/awslabs/goformation/v4/cloudformation/efs"
- "github.com/golang/mock/gomock"
- "github.com/docker/compose-cli/api/compose"
- "github.com/aws/aws-sdk-go/service/elbv2"
- "github.com/awslabs/goformation/v4/cloudformation"
- "github.com/awslabs/goformation/v4/cloudformation/ec2"
- "github.com/awslabs/goformation/v4/cloudformation/ecs"
- "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2"
- "github.com/awslabs/goformation/v4/cloudformation/iam"
- "github.com/awslabs/goformation/v4/cloudformation/logs"
- "github.com/compose-spec/compose-go/loader"
- "github.com/compose-spec/compose-go/types"
- "gotest.tools/v3/assert"
- "gotest.tools/v3/golden"
- )
- func TestSimpleConvert(t *testing.T) {
- bytes, err := ioutil.ReadFile("testdata/input/simple-single-service.yaml")
- assert.NilError(t, err)
- template := convertYaml(t, string(bytes), useDefaultVPC)
- resultAsJSON, err := marshall(template)
- assert.NilError(t, err)
- result := fmt.Sprintf("%s\n", string(resultAsJSON))
- expected := "simple/simple-cloudformation-conversion.golden"
- golden.Assert(t, result, expected)
- }
- func TestLogging(t *testing.T) {
- template := convertYaml(t, `
- services:
- foo:
- image: hello_world
- logging:
- options:
- awslogs-datetime-pattern: "FOO"
- x-aws-logs_retention: 10
- `, useDefaultVPC)
- def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition)
- logging := getMainContainer(def, t).LogConfiguration
- if logging != nil {
- assert.Equal(t, logging.Options["awslogs-datetime-pattern"], "FOO")
- } else {
- t.Fatal("Logging not configured")
- }
- logGroup := template.Resources["LogGroup"].(*logs.LogGroup)
- assert.Equal(t, logGroup.RetentionInDays, 10)
- }
- func TestEnvFile(t *testing.T) {
- template := convertYaml(t, `
- services:
- foo:
- image: hello_world
- env_file:
- - testdata/input/envfile
- `, useDefaultVPC)
- def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition)
- env := getMainContainer(def, t).Environment
- var found bool
- for _, pair := range env {
- if pair.Name == "FOO" {
- assert.Equal(t, pair.Value, "BAR")
- found = true
- }
- }
- assert.Check(t, found, "environment variable FOO not set")
- }
- func TestEnvFileAndEnv(t *testing.T) {
- template := convertYaml(t, `
- services:
- foo:
- image: hello_world
- env_file:
- - testdata/input/envfile
- environment:
- - "FOO=ZOT"
- `, useDefaultVPC)
- def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition)
- env := getMainContainer(def, t).Environment
- var found bool
- for _, pair := range env {
- if pair.Name == "FOO" {
- assert.Equal(t, pair.Value, "ZOT")
- found = true
- }
- }
- assert.Check(t, found, "environment variable FOO not set")
- }
- func TestRollingUpdateLimits(t *testing.T) {
- template := convertYaml(t, `
- services:
- foo:
- image: hello_world
- deploy:
- replicas: 4
- update_config:
- parallelism: 2
- `, useDefaultVPC)
- service := template.Resources["FooService"].(*ecs.Service)
- assert.Check(t, service.DeploymentConfiguration.MaximumPercent == 150)
- assert.Check(t, service.DeploymentConfiguration.MinimumHealthyPercent == 50)
- }
- func TestRollingUpdateExtension(t *testing.T) {
- template := convertYaml(t, `
- services:
- foo:
- image: hello_world
- deploy:
- update_config:
- x-aws-min_percent: 25
- x-aws-max_percent: 125
- `, useDefaultVPC)
- service := template.Resources["FooService"].(*ecs.Service)
- assert.Check(t, service.DeploymentConfiguration.MaximumPercent == 125)
- assert.Check(t, service.DeploymentConfiguration.MinimumHealthyPercent == 25)
- }
- func TestRolePolicy(t *testing.T) {
- template := convertYaml(t, `
- services:
- foo:
- image: hello_world
- x-aws-pull_credentials: "secret"
- `, useDefaultVPC)
- x := template.Resources["FooTaskExecutionRole"]
- assert.Check(t, x != nil)
- role := *(x.(*iam.Role))
- assert.Check(t, role.ManagedPolicyArns[0] == ecsTaskExecutionPolicy)
- assert.Check(t, role.ManagedPolicyArns[1] == ecrReadOnlyPolicy)
- // We expect an extra policy has been created for x-aws-pull_credentials
- assert.Check(t, len(role.Policies) == 1)
- policy := role.Policies[0].PolicyDocument.(*PolicyDocument)
- expected := []string{"secretsmanager:GetSecretValue", "ssm:GetParameters", "kms:Decrypt"}
- assert.DeepEqual(t, expected, policy.Statement[0].Action)
- assert.DeepEqual(t, []string{"secret"}, policy.Statement[0].Resource)
- }
- func TestMapNetworksToSecurityGroups(t *testing.T) {
- template := convertYaml(t, `
- services:
- test:
- image: hello_world
- networks:
- - front-tier
- - back-tier
- networks:
- front-tier:
- name: public
- back-tier:
- internal: true
- `, useDefaultVPC)
- assert.Check(t, template.Resources["FronttierNetwork"] != nil)
- assert.Check(t, template.Resources["BacktierNetwork"] != nil)
- assert.Check(t, template.Resources["BacktierNetworkIngress"] != nil)
- i := template.Resources["FronttierNetworkIngress"]
- assert.Check(t, i != nil)
- ingress := *i.(*ec2.SecurityGroupIngress)
- assert.Check(t, ingress.SourceSecurityGroupId == cloudformation.Ref("FronttierNetwork"))
- }
- func TestLoadBalancerTypeApplication(t *testing.T) {
- cases := []string{
- `services:
- test:
- image: nginx
- ports:
- - 80:80
- `,
- `services:
- test:
- image: nginx
- ports:
- - target: 8080
- x-aws-protocol: http
- `,
- }
- for _, y := range cases {
- template := convertYaml(t, y, useDefaultVPC)
- lb := template.Resources["LoadBalancer"]
- assert.Check(t, lb != nil)
- loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
- assert.Check(t, len(loadBalancer.Name) <= 32)
- assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumApplication)
- assert.Check(t, len(loadBalancer.SecurityGroups) > 0)
- }
- }
- func TestNoLoadBalancerIfNoPortExposed(t *testing.T) {
- template := convertYaml(t, `
- services:
- test:
- image: nginx
- foo:
- image: bar
- `, useDefaultVPC)
- for _, r := range template.Resources {
- assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::TargetGroup")
- assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::Listener")
- assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::PortPublisher")
- }
- }
- func TestServiceReplicas(t *testing.T) {
- template := convertYaml(t, `
- services:
- test:
- image: nginx
- deploy:
- replicas: 10
- `, useDefaultVPC)
- s := template.Resources["TestService"]
- assert.Check(t, s != nil)
- service := *s.(*ecs.Service)
- assert.Check(t, service.DesiredCount == 10)
- }
- func TestTaskSizeConvert(t *testing.T) {
- template := convertYaml(t, `
- services:
- test:
- image: nginx
- `, useDefaultVPC)
- def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
- assert.Equal(t, def.Cpu, "256")
- assert.Equal(t, def.Memory, "512")
- template = convertYaml(t, `
- services:
- test:
- image: nginx
- deploy:
- resources:
- limits:
- cpus: '0.5'
- memory: 2048M
- `, useDefaultVPC)
- def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
- assert.Equal(t, def.Cpu, "512")
- assert.Equal(t, def.Memory, "2048")
- template = convertYaml(t, `
- services:
- test:
- image: nginx
- deploy:
- resources:
- limits:
- cpus: '4'
- memory: 8192M
- `, useDefaultVPC)
- def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
- assert.Equal(t, def.Cpu, "4096")
- assert.Equal(t, def.Memory, "8192")
- template = convertYaml(t, `
- services:
- test:
- image: nginx
- deploy:
- resources:
- limits:
- cpus: '4'
- memory: 792Mb
- reservations:
- generic_resources:
- - discrete_resource_spec:
- kind: gpus
- value: 2
- `, useDefaultVPC, useGPU)
- def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
- assert.Equal(t, def.Cpu, "4000")
- assert.Equal(t, def.Memory, "792")
- template = convertYaml(t, `
- services:
- test:
- image: nginx
- deploy:
- resources:
- reservations:
- generic_resources:
- - discrete_resource_spec:
- kind: gpus
- value: 2
- `, useDefaultVPC, useGPU)
- def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
- assert.Equal(t, def.Cpu, "")
- assert.Equal(t, def.Memory, "")
- }
- func TestLoadBalancerTypeNetwork(t *testing.T) {
- template := convertYaml(t, `
- services:
- test:
- image: nginx
- ports:
- - 80:80
- - 88:88
- `, useDefaultVPC)
- lb := template.Resources["LoadBalancer"]
- assert.Check(t, lb != nil)
- loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer)
- assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumNetwork)
- }
- func TestUseExternalNetwork(t *testing.T) {
- template := convertYaml(t, `
- services:
- test:
- image: nginx
- networks:
- default:
- external: true
- name: sg-123abc
- `, useDefaultVPC, func(m *MockAPIMockRecorder) {
- m.SecurityGroupExists(gomock.Any(), "sg-123abc").Return(true, nil)
- })
- assert.Check(t, template.Resources["DefaultNetwork"] == nil)
- assert.Check(t, template.Resources["DefaultNetworkIngress"] == nil)
- s := template.Resources["TestService"].(*ecs.Service)
- assert.Check(t, s != nil)
- assert.Check(t, s.NetworkConfiguration.AwsvpcConfiguration.SecurityGroups[0] == "sg-123abc") //nolint:staticcheck
- }
- func testVolume(t *testing.T, yaml string, fn ...func(m *MockAPIMockRecorder)) {
- template := convertYaml(t, yaml, fn...)
- s := template.Resources["DbdataNFSMountTargetOnSubnet1"].(*efs.MountTarget)
- assert.Check(t, s != nil)
- assert.Equal(t, s.FileSystemId, "fs-123abc") //nolint:staticcheck
- s = template.Resources["DbdataNFSMountTargetOnSubnet2"].(*efs.MountTarget)
- assert.Check(t, s != nil)
- assert.Equal(t, s.FileSystemId, "fs-123abc") //nolint:staticcheck
- }
- func TestUseExternalVolume(t *testing.T) {
- testVolume(t, `
- services:
- test:
- image: nginx
- volumes:
- db-data:
- external: true
- name: fs-123abc
- `, useDefaultVPC, func(m *MockAPIMockRecorder) {
- m.FileSystemExists(gomock.Any(), "fs-123abc").Return(true, nil)
- })
- }
- func TestCreateVolume(t *testing.T) {
- testVolume(t, `
- services:
- test:
- image: nginx
- volumes:
- db-data: {}
- `, useDefaultVPC, func(m *MockAPIMockRecorder) {
- m.FindFileSystem(gomock.Any(), map[string]string{
- compose.ProjectTag: t.Name(),
- compose.VolumeTag: "db-data",
- }).Return("", nil)
- m.CreateFileSystem(gomock.Any(), map[string]string{
- compose.ProjectTag: t.Name(),
- compose.VolumeTag: "db-data",
- "Name": fmt.Sprintf("%s_%s", t.Name(), "db-data"),
- }).Return("fs-123abc", nil)
- })
- }
- func TestReusePreviousVolume(t *testing.T) {
- testVolume(t, `
- services:
- test:
- image: nginx
- volumes:
- db-data: {}
- `, useDefaultVPC, func(m *MockAPIMockRecorder) {
- m.FindFileSystem(gomock.Any(), map[string]string{
- compose.ProjectTag: t.Name(),
- compose.VolumeTag: "db-data",
- }).Return("fs-123abc", nil)
- })
- }
- func TestServiceMapping(t *testing.T) {
- template := convertYaml(t, `
- services:
- test:
- image: "image"
- command: "command"
- entrypoint: "entrypoint"
- environment:
- - "FOO=BAR"
- cap_add:
- - SYS_PTRACE
- cap_drop:
- - SYSLOG
- init: true
- user: "user"
- working_dir: "working_dir"
- `, useDefaultVPC)
- def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition)
- container := getMainContainer(def, t)
- assert.Equal(t, container.Image, "image")
- assert.Equal(t, container.Command[0], "command")
- assert.Equal(t, container.EntryPoint[0], "entrypoint")
- assert.Equal(t, get(container.Environment, "FOO"), "BAR")
- assert.Check(t, container.LinuxParameters.InitProcessEnabled)
- assert.Equal(t, container.LinuxParameters.Capabilities.Add[0], "SYS_PTRACE")
- assert.Equal(t, container.LinuxParameters.Capabilities.Drop[0], "SYSLOG")
- assert.Equal(t, container.User, "user")
- assert.Equal(t, container.WorkingDirectory, "working_dir")
- }
- func get(l []ecs.TaskDefinition_KeyValuePair, name string) string {
- for _, e := range l {
- if e.Name == name {
- return e.Value
- }
- }
- return ""
- }
- func TestResourcesHaveProjectTagSet(t *testing.T) {
- template := convertYaml(t, `
- services:
- test:
- image: nginx
- ports:
- - 80:80
- - 88:88
- `, useDefaultVPC)
- for _, r := range template.Resources {
- tags := reflect.Indirect(reflect.ValueOf(r)).FieldByName("Tags")
- if !tags.IsValid() {
- continue
- }
- for i := 0; i < tags.Len(); i++ {
- k := tags.Index(i).FieldByName("Key").String()
- v := tags.Index(i).FieldByName("Value").String()
- if k == compose.ProjectTag {
- assert.Equal(t, v, t.Name())
- }
- }
- }
- }
- func convertYaml(t *testing.T, yaml string, fn ...func(m *MockAPIMockRecorder)) *cloudformation.Template {
- project := loadConfig(t, yaml)
- ctrl := gomock.NewController(t)
- defer ctrl.Finish()
- m := NewMockAPI(ctrl)
- for _, f := range fn {
- f(m.EXPECT())
- }
- backend := &ecsAPIService{
- aws: m,
- }
- template, err := backend.convert(context.TODO(), project)
- assert.NilError(t, err)
- return template
- }
- func loadConfig(t *testing.T, yaml string) *types.Project {
- dict, err := loader.ParseYAML([]byte(yaml))
- assert.NilError(t, err)
- model, err := loader.Load(types.ConfigDetails{
- ConfigFiles: []types.ConfigFile{
- {Config: dict},
- },
- }, func(options *loader.Options) {
- options.Name = t.Name()
- })
- assert.NilError(t, err)
- return model
- }
- func getMainContainer(def *ecs.TaskDefinition, t *testing.T) ecs.TaskDefinition_ContainerDefinition {
- for _, c := range def.ContainerDefinitions {
- if c.Essential {
- return c
- }
- }
- t.Fail()
- return def.ContainerDefinitions[0]
- }
- func useDefaultVPC(m *MockAPIMockRecorder) {
- m.GetDefaultVPC(gomock.Any()).Return("vpc-123", nil)
- m.GetSubNets(gomock.Any(), "vpc-123").Return([]string{"subnet1", "subnet2"}, nil)
- }
- func useGPU(m *MockAPIMockRecorder) {
- m.GetParameter(gomock.Any(), gomock.Any()).Return("", nil)
- }
|