package project import ( "io/ioutil" "os" "path/filepath" "reflect" "strings" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/adapter" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/logger" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ecs" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/docker/cli/cli/compose/loader" "github.com/docker/cli/cli/compose/types" "github.com/docker/cli/opts" "github.com/docker/libcompose/yaml" ) const dotEnvFileName = ".env" func (p *ecsProject) parseV3() (*[]adapter.ContainerConfig, error) { log.Debug("Parsing v3 project...") v3Config, err := getV3Config(p.ecsContext.ComposeFiles) if err != nil { return nil, err } servVols, err := adapter.ConvertToV3Volumes(v3Config.Volumes) if err != nil { return nil, err } p.volumes = servVols // convert ServiceConfigs to ContainerConfigs conConfigs := []adapter.ContainerConfig{} for _, service := range v3Config.Services { cCon, err := convertToContainerConfig(service, p.volumes) if err != nil { return nil, err } conConfigs = append(conConfigs, *cCon) } return &conConfigs, nil } // parses compose files into a docker/cli Config, which contains v3 ServiceConfigs func getV3Config(composeFiles []string) (*types.Config, error) { configFiles := []types.ConfigFile{} for _, file := range composeFiles { loadedFile, err := ioutil.ReadFile(file) if err != nil { return nil, err } parsedFile, err := loader.ParseYAML(loadedFile) if err != nil { return nil, err } configFile := types.ConfigFile{ Filename: file, Config: parsedFile, } configFiles = append(configFiles, configFile) } wrkDir, err := getWorkingDir(composeFiles[0]) if err != nil { return nil, err } localEnv := getEnvironment(wrkDir) configDetails := types.ConfigDetails{ WorkingDir: wrkDir, ConfigFiles: configFiles, Environment: localEnv, } // load config from config details config, err := loader.Load(configDetails) if err != nil { return nil, err } return config, nil } func convertToContainerConfig(serviceConfig types.ServiceConfig, serviceVols *adapter.Volumes) (*adapter.ContainerConfig, error) { logger.LogUnsupportedV3ServiceConfigFields(serviceConfig) logWarningForDeployFields(serviceConfig.Deploy, serviceConfig.Name) var stopTimeout *int64 if serviceConfig.StopGracePeriod != nil { st := (int64)(serviceConfig.StopGracePeriod.Seconds()) stopTimeout = &st } c := &adapter.ContainerConfig{ CapAdd: serviceConfig.CapAdd, CapDrop: serviceConfig.CapDrop, Command: serviceConfig.Command, DockerSecurityOptions: serviceConfig.SecurityOpt, Entrypoint: serviceConfig.Entrypoint, Hostname: serviceConfig.Hostname, Image: serviceConfig.Image, Links: serviceConfig.Links, Name: serviceConfig.Name, Privileged: serviceConfig.Privileged, PseudoTerminal: serviceConfig.Tty, ReadOnly: serviceConfig.ReadOnly, StopTimeout: stopTimeout, User: serviceConfig.User, WorkingDirectory: serviceConfig.WorkingDir, } devices, err := adapter.ConvertToDevices(serviceConfig.Devices) if err != nil { return nil, err } c.Devices = devices if serviceConfig.HealthCheck != nil && !serviceConfig.HealthCheck.Disable { c.HealthCheck = adapter.ConvertToHealthCheck(serviceConfig.HealthCheck) } if serviceConfig.DNS != nil { c.DNSServers = serviceConfig.DNS } if serviceConfig.DNSSearch != nil { c.DNSSearchDomains = serviceConfig.DNSSearch } if serviceConfig.Labels != nil { labelsMap := aws.StringMap(serviceConfig.Labels) c.DockerLabels = labelsMap } envVars := []*ecs.KeyValuePair{} for k, v := range serviceConfig.Environment { env := ecs.KeyValuePair{} env.SetName(k) if v != nil { env.SetValue(*v) } else { env.SetValue("") } envVars = append(envVars, &env) } c.Environment = envVars extraHosts, err := adapter.ConvertToExtraHosts(serviceConfig.ExtraHosts) if err != nil { return nil, err } c.ExtraHosts = extraHosts // TODO: refactor adapter.ConvertToLogConfiguration to take in driver (string) and Options (map[string]string) if serviceConfig.Logging != nil { logConfig := ecs.LogConfiguration{} logConfig.SetLogDriver(serviceConfig.Logging.Driver) optionsMap := aws.StringMap(serviceConfig.Logging.Options) logConfig.SetOptions(optionsMap) c.LogConfiguration = &logConfig } if len(serviceConfig.Ports) > 0 { var portMappings []*ecs.PortMapping for _, portConfig := range serviceConfig.Ports { mapping := convertPortConfigToECSMapping(portConfig) portMappings = append(portMappings, mapping) } c.PortMappings = portMappings } // TODO: change ConvertToTmpfs to take in []string if serviceConfig.Tmpfs != nil { tmpfs := yaml.Stringorslice(serviceConfig.Tmpfs) ecsTmpfs, err := adapter.ConvertToTmpfs(tmpfs) if err != nil { return nil, err } c.Tmpfs = ecsTmpfs } if len(serviceConfig.Ulimits) > 0 { c.Ulimits = convertToECSUlimits(serviceConfig.Ulimits) } if len(serviceConfig.Volumes) > 0 { mountPoints := []*ecs.MountPoint{} for _, volConfig := range serviceConfig.Volumes { if volConfig.Type == "volume" || volConfig.Type == "bind" { sourceVolName, err := adapter.GetSourcePathAndUpdateVolumes(volConfig.Source, serviceVols) if err != nil { return nil, err } containerPath := volConfig.Target readOnly := volConfig.ReadOnly mp := &ecs.MountPoint{ ContainerPath: &containerPath, SourceVolume: &sourceVolName, ReadOnly: &readOnly, } mountPoints = append(mountPoints, mp) } else { log.Warnf("Unsupported mount type found: %s", volConfig.Type) } } c.MountPoints = mountPoints } return c, nil } func getWorkingDir(fileName string) (string, error) { pwd, err := filepath.Abs(fileName) if err != nil { return "", errors.Wrap(err, "Unable to retrieve compose file directory") } return filepath.Dir(pwd), nil } func convertPortConfigToECSMapping(portConfig types.ServicePortConfig) *ecs.PortMapping { containerPort := int64(portConfig.Target) hostPort := int64(portConfig.Published) var ecsMapping = ecs.PortMapping{ ContainerPort: &containerPort, HostPort: &hostPort, Protocol: &portConfig.Protocol, } return &ecsMapping } func convertToECSUlimits(ulimits map[string]*types.UlimitsConfig) []*ecs.Ulimit { ecsUlimits := []*ecs.Ulimit{} for name, ulimit := range ulimits { ecsULimit := ecs.Ulimit{} ecsULimit.SetName(name) if ulimit.Single > 0 { ecsULimit.SetSoftLimit(int64(ulimit.Single)) ecsULimit.SetHardLimit(int64(ulimit.Single)) } else { ecsULimit.SetSoftLimit(int64(ulimit.Soft)) ecsULimit.SetHardLimit(int64(ulimit.Hard)) } ecsUlimits = append(ecsUlimits, &ecsULimit) } return ecsUlimits } func logWarningForDeployFields(d types.DeployConfig, serviceName string) { if d.Resources.Limits != nil || d.Resources.Reservations != nil { log.WithFields(log.Fields{ "option name": "deploy", "service name": serviceName, }).Warn("Skipping unsupported YAML option for service... service-level resources should be configured in the ecs-param.yml file.") } else if !reflect.DeepEqual(d, types.DeployConfig{}) { log.WithFields(log.Fields{ "option name": "deploy", "service name": serviceName, }).Warn("Skipping unsupported YAML option for service...") } } func getEnvironment(wrkDir string) map[string]string { env, err := opts.ParseEnvFile(filepath.Join(wrkDir, dotEnvFileName)) if err == nil { env = append(env, os.Environ()...) } else { env = os.Environ() } if err != nil && !os.IsNotExist(err) { // we are not even sure whether .env file is there log.WithFields(log.Fields{ "cause": err.Error(), }).Warn("failed to access .env") } envMap := make(map[string]string, len(env)) for _, s := range env { varParts := strings.SplitN(s, "=", 2) envMap[varParts[0]] = varParts[1] } return envMap }