package project import ( "fmt" "io/ioutil" "os" "testing" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/adapter" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ecs" "github.com/stretchr/testify/assert" ) func TestParseV1V2_Version1_HappyPath(t *testing.T) { // expected ContainerConfig values redisImage := "redis" webImage := "web" cpuShares := int64(73) command := []string{"bundle exec thin -p 3000"} devices := []*ecs.Device{ { HostPath: aws.String("/dev/sda"), ContainerPath: aws.String("/dev/sdd"), Permissions: aws.StringSlice([]string{"read"}), }, { HostPath: aws.String("/dev/sdd"), ContainerPath: aws.String("/dev/xdr"), }, { HostPath: aws.String("/dev/sda"), }, } dnsServers := []string{"1.2.3.4"} dnsSearchDomains := []string{"search.example.com"} entryPoint := []string{"/code/entrypoint.sh"} env := []*ecs.KeyValuePair{ { Name: aws.String("RACK_ENV"), Value: aws.String("development"), }, } extraHosts := []*ecs.HostEntry{ { Hostname: aws.String("test.local"), IpAddress: aws.String("127.10.10.10"), }, } hostname := "foobarbaz" labels := map[string]*string{ "label1": aws.String(""), "com.foo.label2": aws.String("value"), } links := []string{"redis:redis"} logDriver := aws.String("json-file") logOpts := map[string]*string{ "max-file": aws.String("50"), "max-size": aws.String("50k"), } logging := &ecs.LogConfiguration{ LogDriver: logDriver, Options: logOpts, } memory := int64(953) // 1000000000 in Mib mountPoints := []*ecs.MountPoint{ { ContainerPath: aws.String("./code"), ReadOnly: aws.Bool(false), SourceVolume: aws.String("volume-0"), }, } ports := []*ecs.PortMapping{ { ContainerPort: aws.Int64(5000), HostPort: aws.Int64(5000), Protocol: aws.String("tcp"), }, { ContainerPort: aws.Int64(8001), HostPort: aws.Int64(8001), Protocol: aws.String("tcp"), }, } privileged := true readonly := true tty := true securityOpts := []string{"label:type:test_virt"} shmSize := int64(128) user := "user" ulimits := []*ecs.Ulimit{ { Name: aws.String("nproc"), HardLimit: aws.Int64(65535), SoftLimit: aws.Int64(65535), }, { Name: aws.String("nofile"), HardLimit: aws.Int64(40000), SoftLimit: aws.Int64(20000), }, } volumesFrom := []*ecs.VolumeFrom{ { ReadOnly: aws.Bool(true), SourceContainer: aws.String("web"), }, } workingDir := "/var" composeV1FileString := `web: cpu_shares: 73 command: - bundle exec thin -p 3000 devices: - "/dev/sda:/dev/sdd:r" - "/dev/sdd:/dev/xdr" - "/dev/sda" dns: - 1.2.3.4 dns_search: search.example.com entrypoint: /code/entrypoint.sh environment: RACK_ENV: development extra_hosts: - test.local:127.10.10.10 hostname: "foobarbaz" image: web labels: - label1 - com.foo.label2=value links: - "redis:redis" log_driver: json-file log_opt: max-file: 50 max-size: 50k mem_limit: 1000000000 ports: - '5000:5000' - "127.0.0.1:8001:8001" privileged: true read_only: true security_opt: - label:type:test_virt shm_size: 128M tty: true ulimits: nproc: 65535 nofile: soft: 20000 hard: 40000 user: user volumes: - ./code working_dir: /var redis: image: redis volumes_from: - web:ro` // Setup docker-compose file tmpfile, err := ioutil.TempFile("", "test") assert.NoError(t, err, "Unexpected error in creating test file") defer os.Remove(tmpfile.Name()) _, err = tmpfile.Write([]byte(composeV1FileString)) assert.NoError(t, err, "Unexpected error in writing to test file") err = tmpfile.Close() assert.NoError(t, err, "Unexpected error closing file") // Set up project project := setupTestProject(t) project.ecsContext.ComposeFiles = append(project.ecsContext.ComposeFiles, tmpfile.Name()) // Parse docker compose file actualConfigs, err := project.parseV1V2() assert.NoError(t, err, "Unexpected error parsing file") // verify redis ContainerConfig redis, err := getContainerConfigByName("redis", actualConfigs) assert.NoError(t, err, "Unexpected error retrieving redis config") assert.Equal(t, redisImage, redis.Image, "Expected image to match") assert.Equal(t, volumesFrom, redis.VolumesFrom, "Expected VolumesFrom to match") // verify web ContainerConfig web, err := getContainerConfigByName("web", actualConfigs) assert.NoError(t, err, "Unexpected error retrieving web config") assert.Equal(t, command, web.Command, "Expected Command to match") assert.Equal(t, cpuShares, web.CPU, "Expected CPU to match") assert.ElementsMatch(t, devices, web.Devices, "Expected Devices to match") assert.Equal(t, dnsSearchDomains, web.DNSSearchDomains, "Expected DNSSearchDomains to match") assert.Equal(t, dnsServers, web.DNSServers, "Expected DNSServers to match") assert.Equal(t, labels, web.DockerLabels, "Expected DockerLabels to match") assert.Equal(t, securityOpts, web.DockerSecurityOptions, "Expected DockerSecurityOptions to match") assert.Equal(t, entryPoint, web.Entrypoint, "Expected EntryPoint to be match") assert.Equal(t, env, web.Environment, "Expected Environment to match") assert.Equal(t, extraHosts, web.ExtraHosts, "Expected ExtraHosts to match") assert.Equal(t, hostname, web.Hostname, "Expected Hostname to match") assert.Equal(t, webImage, web.Image, "Expected Image to match") assert.Equal(t, links, web.Links, "Expected Links to match") assert.Equal(t, logging, web.LogConfiguration, "Expected LogConfiguration to match") assert.Equal(t, memory, web.Memory, "Expected Memory to match") assert.Equal(t, mountPoints, web.MountPoints, "Expected MountPoints to match") assert.ElementsMatch(t, ports, web.PortMappings, "Expected PortMappings to match") assert.Equal(t, privileged, web.Privileged, "Expected Privileged to match") assert.Equal(t, tty, web.PseudoTerminal, "Expected PseudoTerminal to match") assert.Equal(t, readonly, web.ReadOnly, "Expected ReadOnly to match") assert.Equal(t, shmSize, web.ShmSize, "Expected ShmSize to match") assert.Equal(t, user, web.User, "Expected User to match") assert.ElementsMatch(t, ulimits, web.Ulimits, "Expected Ulimits to match") assert.Equal(t, workingDir, web.WorkingDirectory, "Expected WorkingDirectory to match") } func TestParseV1V2_Version2Files(t *testing.T) { // expected ContainerConfig values wordpressImage := "wordpress" mysqlImage := "mysql" capAdd := []string{"ALL"} capDrop := []string{"NET_ADMIN", "SYS_ADMIN"} devices := []*ecs.Device{ { HostPath: aws.String("/dev/sda"), ContainerPath: aws.String("/dev/sdd"), Permissions: aws.StringSlice([]string{"read"}), }, { HostPath: aws.String("/dev/sdd"), ContainerPath: aws.String("/dev/xdr"), }, { HostPath: aws.String("/dev/sda"), }, } logOpts := map[string]*string{ "syslog-address": aws.String("tcp://192.168.0.42:123"), } logging := &ecs.LogConfiguration{ LogDriver: aws.String("syslog"), Options: logOpts, } memoryReservation := int64(476) // 500000000 / miB memory := int64(512) mountPoints := []*ecs.MountPoint{ { ContainerPath: aws.String("/var/lib/mysql"), ReadOnly: aws.Bool(false), SourceVolume: aws.String("volume-1"), }, { ContainerPath: aws.String("/var/lib/mysql"), ReadOnly: aws.Bool(false), SourceVolume: aws.String("volume-2"), }, { ContainerPath: aws.String("/tmp/cache"), ReadOnly: aws.Bool(false), SourceVolume: aws.String("volume-3"), }, { ContainerPath: aws.String("/etc/configs/"), ReadOnly: aws.Bool(true), SourceVolume: aws.String("volume-4"), }, { ContainerPath: aws.String("/var/lib/mysql"), ReadOnly: aws.Bool(false), SourceVolume: aws.String("datavolume"), }, } ports := []*ecs.PortMapping{ { ContainerPort: aws.Int64(80), HostPort: aws.Int64(80), Protocol: aws.String("tcp"), }, } shmSize := int64(1024) // 1 gb = 1024 miB stg := int64(60) stopGracePeriod := &stg tmpfs := []*ecs.Tmpfs{ { ContainerPath: aws.String("/run"), MountOptions: aws.StringSlice([]string{}), Size: aws.Int64(1024), }, { ContainerPath: aws.String("/tmp"), MountOptions: aws.StringSlice([]string{"ro", "rw"}), Size: aws.Int64(64), }, } volumesFrom := []*ecs.VolumeFrom{{ ReadOnly: aws.Bool(false), SourceContainer: aws.String("mysql"), }, } composeV2FileString := `version: '2' services: wordpress: cap_add: - ALL cap_drop: - NET_ADMIN - SYS_ADMIN devices: - "/dev/sda:/dev/sdd:r" - "/dev/sdd:/dev/xdr" - "/dev/sda" image: wordpress ports: ["80:80"] mem_reservation: 500000000 mem_limit: 512M shm_size: 1gb stop_grace_period: "1m" tmpfs: - /run:size=1gb - /tmp:size=65536k,ro,rw logging: driver: syslog options: syslog-address: "tcp://192.168.0.42:123" volumes_from: - mysql:rw mysql: image: mysql volumes: - /var/lib/mysql - /opt/data:/var/lib/mysql - ./cache:/tmp/cache:rw - ~/configs:/etc/configs/:ro - datavolume:/var/lib/mysql volumes: datavolume:` // Setup docker-compose file tmpfile, err := ioutil.TempFile("", "test") assert.NoError(t, err, "Unexpected error in creating test file") defer os.Remove(tmpfile.Name()) _, err = tmpfile.Write([]byte(composeV2FileString)) assert.NoError(t, err, "Unexpected error in writing to test file") err = tmpfile.Close() assert.NoError(t, err, "Unexpected error closing file") // Set up project project := setupTestProject(t) project.ecsContext.ComposeFiles = append(project.ecsContext.ComposeFiles, tmpfile.Name()) // Parse docker compose file actualConfigs, err := project.parseV1V2() assert.NoError(t, err, "Unexpected error parsing file") // verify wordpress ContainerConfig wordpress, err := getContainerConfigByName("wordpress", actualConfigs) assert.NoError(t, err, "Unexpected error retrieving wordpress config") assert.Equal(t, wordpressImage, wordpress.Image, "Expected wordpress Image to match") assert.Equal(t, capAdd, wordpress.CapAdd, "Expected CapAdd to match") assert.Equal(t, capDrop, wordpress.CapDrop, "Expected CapDrop to match") assert.ElementsMatch(t, devices, wordpress.Devices, "Expected Devices to match") assert.Equal(t, logging, wordpress.LogConfiguration, "Expected Log Configuration to match") assert.Equal(t, memoryReservation, wordpress.MemoryReservation, "Expected memoryReservation to match") assert.Equal(t, memory, wordpress.Memory, "Expected Memory to match") assert.Equal(t, ports, wordpress.PortMappings, "Expected ports to match") assert.Equal(t, shmSize, wordpress.ShmSize, "Expected shmSize to match") assert.Equal(t, stopGracePeriod, wordpress.StopTimeout, "Expected StopTimeout to match") assert.ElementsMatch(t, tmpfs, wordpress.Tmpfs, "Expected tmpfs to match") assert.Equal(t, volumesFrom, wordpress.VolumesFrom, "Expected VolumesFrom to match") // verify mysql ContainerConfig mysql, err := getContainerConfigByName("mysql", actualConfigs) assert.NoError(t, err, "Unexpected error retrieving wordpress config") assert.Equal(t, mysqlImage, mysql.Image, "Expected mysql Image to match") assert.ElementsMatch(t, mountPoints, mysql.MountPoints, "Expected MountPoints to match") } func TestParseV1V2_Version1_WithEnvFile(t *testing.T) { // Set up env file envKey := "rails_env" envValue := "development" envContents := []byte(envKey + "=" + envValue) envFile, err := ioutil.TempFile("", "example") assert.NoError(t, err, "Unexpected error in creating test env file") defer os.Remove(envFile.Name()) _, err = envFile.Write(envContents) assert.NoError(t, err, "Unexpected error in writing to test env file") expectedEnv := []*ecs.KeyValuePair{ { Name: aws.String(envKey), Value: aws.String(envValue), }, } // Setup docker-compose file webImage := "webapp" composeFileString := `web: image: webapp env_file: - ` + envFile.Name() tmpfile, err := ioutil.TempFile("", "test") assert.NoError(t, err, "Unexpected error in creating test file") defer os.Remove(tmpfile.Name()) _, err = tmpfile.Write([]byte(composeFileString)) assert.NoError(t, err, "Unexpected error in writing to test file") err = tmpfile.Close() assert.NoError(t, err, "Unexpected error closing file") // Set up project project := setupTestProject(t) project.ecsContext.ComposeFiles = append(project.ecsContext.ComposeFiles, tmpfile.Name()) actualConfigs, err := project.parseV1V2() assert.NoError(t, err, "Unexpected error parsing file") // verify wordpress ServiceConfig web, err := getContainerConfigByName("web", actualConfigs) assert.NoError(t, err, "Unexpected error retrieving container config") assert.Equal(t, webImage, web.Image, "Expected Image to match") assert.Equal(t, expectedEnv, web.Environment, "Expected Environment to match") } func TestParseV1V2_WithEnvFromShell(t *testing.T) { // If the value for a key in the environment field is blank, it // resolves to the shell value (good for specifying secrets). // If a value is specified for a key in the environment field, it // overrides the shell value for that key. envKey1 := "RACK_ENV" envValue1 := "staging" envKey2 := "SHOW" envValue2 := "true" envKey3 := "SESSION_SECRET" envValue3 := "clydeIsTheGoodestDog" os.Setenv(envKey1, envValue1) os.Setenv(envKey2, envValue2) os.Setenv(envKey3, envValue3) defer func() { os.Unsetenv(envKey1) os.Unsetenv(envKey2) os.Unsetenv(envKey3) }() expectedEnv := []*ecs.KeyValuePair{ { Name: aws.String(envKey1), Value: aws.String("development"), }, { Name: aws.String(envKey2), Value: aws.String(envValue2), }, { Name: aws.String(envKey3), Value: aws.String(envValue3), }, } // Setup docker-compose file webImage := "webapp" composeFileString := `version: '2' services: web: image: webapp environment: - RACK_ENV=development - SHOW= - SESSION_SECRET` tmpfile, err := ioutil.TempFile("", "test") assert.NoError(t, err, "Unexpected error in creating test file") defer os.Remove(tmpfile.Name()) _, err = tmpfile.Write([]byte(composeFileString)) assert.NoError(t, err, "Unexpected error in writing to test file") err = tmpfile.Close() assert.NoError(t, err, "Unexpected error closing file") // Set up project project := setupTestProject(t) project.ecsContext.ComposeFiles = append(project.ecsContext.ComposeFiles, tmpfile.Name()) actualConfigs, err := project.parseV1V2() assert.NoError(t, err, "Unexpected error parsing file") // verify wordpress ServiceConfig web, err := getContainerConfigByName("web", actualConfigs) assert.NoError(t, err, "Unexpected error retrieving container config") assert.Equal(t, webImage, web.Image, "Expected Image to match") assert.Equal(t, expectedEnv, web.Environment, "Expected Environment to match") } func TestParseV1V2_MemoryValidation(t *testing.T) { // Setup docker-compose file memory := int64(128) composeFileString := `version: '2' services: web: image: webapp mem_reservation: 128m` tmpfile, err := ioutil.TempFile("", "test") assert.NoError(t, err, "Unexpected error in creating test file") defer os.Remove(tmpfile.Name()) _, err = tmpfile.Write([]byte(composeFileString)) assert.NoError(t, err, "Unexpected error in writing to test file") err = tmpfile.Close() assert.NoError(t, err, "Unexpected error closing file") // Set up project project := setupTestProject(t) project.ecsContext.ComposeFiles = append(project.ecsContext.ComposeFiles, tmpfile.Name()) actualConfigs, err := project.parseV1V2() assert.NoError(t, err, "Unexpected error parsing file") // verify wordpress ServiceConfig web, err := getContainerConfigByName("web", actualConfigs) if assert.NoError(t, err) { assert.Equal(t, memory, web.Memory, "Expected Memory to match") assert.Equal(t, memory, web.MemoryReservation, "Expected MemoryReservation to match") } } func getContainerConfigByName(name string, configs *[]adapter.ContainerConfig) (*adapter.ContainerConfig, error) { for _, config := range *configs { if config.Name == name { return &config, nil } } return nil, fmt.Errorf("Container with name %v could not be found", name) }