|
| 1 | +package adapter |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "fmt" |
| 6 | + "regexp" |
| 7 | + "strconv" |
| 8 | + "strings" |
| 9 | + |
| 10 | + "github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils" |
| 11 | + "github.com/aws/aws-sdk-go/aws" |
| 12 | + "github.com/aws/aws-sdk-go/service/ecs" |
| 13 | + "github.com/docker/go-units" |
| 14 | + "github.com/docker/libcompose/config" |
| 15 | + "github.com/docker/libcompose/project" |
| 16 | + "github.com/docker/libcompose/yaml" |
| 17 | + "github.com/pkg/errors" |
| 18 | + log "github.com/sirupsen/logrus" |
| 19 | +) |
| 20 | + |
| 21 | +const ( |
| 22 | + kiB = 1024 |
| 23 | + miB = kiB * kiB // 1048576 bytes |
| 24 | + |
| 25 | + // access mode with which the volume is mounted |
| 26 | + readOnlyVolumeAccessMode = "ro" |
| 27 | + readWriteVolumeAccessMode = "rw" |
| 28 | + volumeFromContainerKey = "container" |
| 29 | +) |
| 30 | + |
| 31 | +// ConvertToExtraHosts transforms the yml extra hosts slice to ecs compatible HostEntry slice |
| 32 | +func ConvertToExtraHosts(cfgExtraHosts []string) ([]*ecs.HostEntry, error) { |
| 33 | + extraHosts := []*ecs.HostEntry{} |
| 34 | + for _, cfgExtraHost := range cfgExtraHosts { |
| 35 | + parts := strings.Split(cfgExtraHost, ":") |
| 36 | + if len(parts) != 2 { |
| 37 | + return nil, fmt.Errorf( |
| 38 | + "expected format HOSTNAME:IPADDRESS. could not parse ExtraHost: %s", cfgExtraHost) |
| 39 | + } |
| 40 | + extraHost := &ecs.HostEntry{ |
| 41 | + Hostname: aws.String(parts[0]), |
| 42 | + IpAddress: aws.String(parts[1]), |
| 43 | + } |
| 44 | + extraHosts = append(extraHosts, extraHost) |
| 45 | + } |
| 46 | + |
| 47 | + return extraHosts, nil |
| 48 | +} |
| 49 | + |
| 50 | +// ConvertToKeyValuePairs transforms the map of environment variables into list of ecs.KeyValuePair. |
| 51 | +// Environment variables with only a key are resolved by reading the variable from the shell where ecscli is executed from. |
| 52 | +// TODO: use this logic to generate RunTask overrides for ecscli compose commands (instead of always creating a new task def) |
| 53 | +func ConvertToKeyValuePairs(context *project.Context, envVars yaml.MaporEqualSlice, serviceName string) []*ecs.KeyValuePair { |
| 54 | + environment := []*ecs.KeyValuePair{} |
| 55 | + for _, env := range envVars { |
| 56 | + parts := strings.SplitN(env, "=", 2) |
| 57 | + key := parts[0] |
| 58 | + |
| 59 | + // format: key=value |
| 60 | + if len(parts) > 1 && parts[1] != "" { |
| 61 | + environment = append(environment, createKeyValuePair(key, parts[1])) |
| 62 | + continue |
| 63 | + } |
| 64 | + |
| 65 | + // format: key |
| 66 | + // format: key= |
| 67 | + if context.EnvironmentLookup != nil { |
| 68 | + resolvedEnvVars := context.EnvironmentLookup.Lookup(key, nil) |
| 69 | + |
| 70 | + // If the environment variable couldn't be resolved, set the value to an empty string |
| 71 | + // Reference: https://github.com/docker/libcompose/blob/3c40e1001a2646ec6f7a6613873cf5a30122a417/config/interpolation.go#L148 |
| 72 | + if len(resolvedEnvVars) == 0 { |
| 73 | + log.WithFields(log.Fields{"key name": key}).Warn("Environment variable is unresolved. Setting it to a blank value...") |
| 74 | + environment = append(environment, createKeyValuePair(key, "")) |
| 75 | + continue |
| 76 | + } |
| 77 | + |
| 78 | + // Use first result if many are given |
| 79 | + value := resolvedEnvVars[0] |
| 80 | + lookupParts := strings.SplitN(value, "=", 2) |
| 81 | + environment = append(environment, createKeyValuePair(key, lookupParts[1])) |
| 82 | + } |
| 83 | + } |
| 84 | + return environment |
| 85 | +} |
| 86 | + |
| 87 | +// createKeyValuePair generates an ecs.KeyValuePair object |
| 88 | +func createKeyValuePair(key, value string) *ecs.KeyValuePair { |
| 89 | + return &ecs.KeyValuePair{ |
| 90 | + Name: aws.String(key), |
| 91 | + Value: aws.String(value), |
| 92 | + } |
| 93 | +} |
| 94 | + |
| 95 | +// ConvertToLogConfiguration converts a libcompose logging |
| 96 | +// fields to an ECS LogConfiguration |
| 97 | +func ConvertToLogConfiguration(inputCfg *config.ServiceConfig) (*ecs.LogConfiguration, error) { |
| 98 | + var logConfig *ecs.LogConfiguration |
| 99 | + if inputCfg.Logging.Driver != "" { |
| 100 | + logConfig = &ecs.LogConfiguration{ |
| 101 | + LogDriver: aws.String(inputCfg.Logging.Driver), |
| 102 | + Options: aws.StringMap(inputCfg.Logging.Options), |
| 103 | + } |
| 104 | + } |
| 105 | + return logConfig, nil |
| 106 | +} |
| 107 | + |
| 108 | +// ConvertToMemoryInMB converts libcompose-parsed bytes to MiB, expected by ECS |
| 109 | +func ConvertToMemoryInMB(bytes int64) int64 { |
| 110 | + var memory int64 |
| 111 | + if bytes != 0 { |
| 112 | + memory = int64(bytes) / miB |
| 113 | + } |
| 114 | + return memory |
| 115 | +} |
| 116 | + |
| 117 | +// ConvertToMountPoints transforms the yml volumes slice to ecs compatible MountPoints slice |
| 118 | +// It also uses the hostPath from volumes if present, else adds one to it |
| 119 | +func ConvertToMountPoints(cfgVolumes *yaml.Volumes, volumes *Volumes) ([]*ecs.MountPoint, error) { |
| 120 | + mountPoints := []*ecs.MountPoint{} |
| 121 | + if cfgVolumes == nil { |
| 122 | + return mountPoints, nil |
| 123 | + } |
| 124 | + for _, cfgVolume := range cfgVolumes.Volumes { |
| 125 | + source := cfgVolume.Source |
| 126 | + containerPath := cfgVolume.Destination |
| 127 | + |
| 128 | + accessMode := cfgVolume.AccessMode |
| 129 | + var readOnly bool |
| 130 | + if accessMode != "" { |
| 131 | + if accessMode == readOnlyVolumeAccessMode { |
| 132 | + readOnly = true |
| 133 | + } else if accessMode == readWriteVolumeAccessMode { |
| 134 | + readOnly = false |
| 135 | + } else { |
| 136 | + return nil, fmt.Errorf( |
| 137 | + "expected format [HOST:]CONTAINER[:ro|rw]. could not parse volume: %s", cfgVolume) |
| 138 | + } |
| 139 | + } |
| 140 | + |
| 141 | + var volumeName string |
| 142 | + numVol := len(volumes.VolumeWithHost) + len(volumes.VolumeEmptyHost) |
| 143 | + if source == "" { |
| 144 | + // add mount point for volumes with an empty host path |
| 145 | + volumeName = getVolumeName(numVol) |
| 146 | + volumes.VolumeEmptyHost = append(volumes.VolumeEmptyHost, volumeName) |
| 147 | + } else if project.IsNamedVolume(source) { |
| 148 | + if !utils.InSlice(source, volumes.VolumeEmptyHost) { |
| 149 | + return nil, fmt.Errorf( |
| 150 | + "named volume [%s] is used but no declaration was found in the volumes section", cfgVolume) |
| 151 | + } |
| 152 | + volumeName = source |
| 153 | + } else { |
| 154 | + // add mount point for volumes with a host path |
| 155 | + volumeName = volumes.VolumeWithHost[source] |
| 156 | + |
| 157 | + if volumeName == "" { |
| 158 | + volumeName = getVolumeName(numVol) |
| 159 | + volumes.VolumeWithHost[source] = volumeName |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + mountPoints = append(mountPoints, &ecs.MountPoint{ |
| 164 | + ContainerPath: aws.String(containerPath), |
| 165 | + SourceVolume: aws.String(volumeName), |
| 166 | + ReadOnly: aws.Bool(readOnly), |
| 167 | + }) |
| 168 | + } |
| 169 | + return mountPoints, nil |
| 170 | +} |
| 171 | + |
| 172 | +// ConvertToPortMappings transforms the yml ports string slice to ecs compatible PortMappings slice |
| 173 | +func ConvertToPortMappings(serviceName string, cfgPorts []string) ([]*ecs.PortMapping, error) { |
| 174 | + portMappings := []*ecs.PortMapping{} |
| 175 | + for _, portMapping := range cfgPorts { |
| 176 | + // TODO: suffix-check case insensitive? |
| 177 | + |
| 178 | + // Example format "8000:8000/udp" |
| 179 | + protocol := ecs.TransportProtocolTcp // default protocol:tcp |
| 180 | + tcp := strings.HasSuffix(portMapping, "/"+ecs.TransportProtocolTcp) |
| 181 | + udp := strings.HasSuffix(portMapping, "/"+ecs.TransportProtocolUdp) |
| 182 | + if tcp || udp { |
| 183 | + protocol = portMapping[len(portMapping)-3:] // slice protocol name from portMapping, 3=len(ecs.TransportProtocolTcp) |
| 184 | + portMapping = portMapping[0 : len(portMapping)-4] |
| 185 | + } |
| 186 | + |
| 187 | + // either has 1 part (just the containerPort) or has 2 parts (hostPort:containerPort) |
| 188 | + parts := strings.Split(portMapping, ":") |
| 189 | + var containerPort, hostPort int |
| 190 | + var portErr error |
| 191 | + switch len(parts) { |
| 192 | + case 1: // Format "containerPort" Example "8000" |
| 193 | + containerPort, portErr = strconv.Atoi(parts[0]) |
| 194 | + case 2: // Format "hostPort:containerPort" Example "8000:8000" |
| 195 | + hostPort, portErr = strconv.Atoi(parts[0]) |
| 196 | + containerPort, portErr = strconv.Atoi(parts[1]) |
| 197 | + case 3: // Format "ipAddr:hostPort:containerPort" Example "127.0.0.0.1:8000:8000" |
| 198 | + log.WithFields(log.Fields{ |
| 199 | + "container": serviceName, |
| 200 | + "portMapping": portMapping, |
| 201 | + }).Warn("Ignoring the ip address while transforming it to task definition") |
| 202 | + hostPort, portErr = strconv.Atoi(parts[1]) |
| 203 | + containerPort, portErr = strconv.Atoi(parts[2]) |
| 204 | + default: |
| 205 | + return nil, fmt.Errorf( |
| 206 | + "expected format [hostPort]:containerPort. Could not parse portmappings: %s", portMapping) |
| 207 | + } |
| 208 | + if portErr != nil { |
| 209 | + return nil, fmt.Errorf("Could not convert port into integer in portmappings: %v", portErr) |
| 210 | + } |
| 211 | + |
| 212 | + portMappings = append(portMappings, &ecs.PortMapping{ |
| 213 | + Protocol: aws.String(protocol), |
| 214 | + ContainerPort: aws.Int64(int64(containerPort)), |
| 215 | + HostPort: aws.Int64(int64(hostPort)), |
| 216 | + }) |
| 217 | + } |
| 218 | + return portMappings, nil |
| 219 | +} |
| 220 | + |
| 221 | +// ConvertToTmpfs transforms the yml Tmpfs slice of strings to slice of pointers to Tmpfs structs |
| 222 | +func ConvertToTmpfs(tmpfsPaths yaml.Stringorslice) ([]*ecs.Tmpfs, error) { |
| 223 | + |
| 224 | + if len(tmpfsPaths) == 0 { |
| 225 | + return nil, nil |
| 226 | + } |
| 227 | + |
| 228 | + mounts := []*ecs.Tmpfs{} |
| 229 | + for _, mount := range tmpfsPaths { |
| 230 | + |
| 231 | + // mount should be of the form "<path>:<options>" |
| 232 | + tmpfsParams := strings.SplitN(mount, ":", 2) |
| 233 | + |
| 234 | + if len(tmpfsParams) < 2 { |
| 235 | + return nil, errors.New("Path and Size are required options for tmpfs") |
| 236 | + } |
| 237 | + |
| 238 | + path := tmpfsParams[0] |
| 239 | + options := strings.Split(tmpfsParams[1], ",") |
| 240 | + |
| 241 | + var mountOptions []string |
| 242 | + var size int64 |
| 243 | + |
| 244 | + // See: https://github.com/docker/go-units/blob/master/size.go#L34 |
| 245 | + s := regexp.MustCompile(`size=(\d+(\.\d+)*) ?([kKmMgGtTpP])?[bB]?`) |
| 246 | + |
| 247 | + for _, option := range options { |
| 248 | + if sizeOption := s.FindString(option); sizeOption != "" { |
| 249 | + sizeValue := strings.SplitN(sizeOption, "=", 2)[1] |
| 250 | + sizeInBytes, err := units.RAMInBytes(sizeValue) |
| 251 | + |
| 252 | + if err != nil { |
| 253 | + return nil, err |
| 254 | + } |
| 255 | + |
| 256 | + size = sizeInBytes / miB |
| 257 | + } else { |
| 258 | + mountOptions = append(mountOptions, option) |
| 259 | + } |
| 260 | + } |
| 261 | + |
| 262 | + if size == 0 { |
| 263 | + return nil, errors.New("You must specify the size option for tmpfs") |
| 264 | + } |
| 265 | + |
| 266 | + tmpfs := &ecs.Tmpfs{ |
| 267 | + ContainerPath: aws.String(path), |
| 268 | + MountOptions: aws.StringSlice(mountOptions), |
| 269 | + Size: aws.Int64(size), |
| 270 | + } |
| 271 | + mounts = append(mounts, tmpfs) |
| 272 | + } |
| 273 | + return mounts, nil |
| 274 | +} |
| 275 | + |
| 276 | +// ConvertToULimits transforms the yml extra hosts slice to ecs compatible Ulimit slice |
| 277 | +func ConvertToULimits(cfgUlimits yaml.Ulimits) ([]*ecs.Ulimit, error) { |
| 278 | + ulimits := []*ecs.Ulimit{} |
| 279 | + for _, cfgUlimit := range cfgUlimits.Elements { |
| 280 | + ulimit := &ecs.Ulimit{ |
| 281 | + Name: aws.String(cfgUlimit.Name), |
| 282 | + SoftLimit: aws.Int64(cfgUlimit.Soft), |
| 283 | + HardLimit: aws.Int64(cfgUlimit.Hard), |
| 284 | + } |
| 285 | + ulimits = append(ulimits, ulimit) |
| 286 | + } |
| 287 | + |
| 288 | + return ulimits, nil |
| 289 | +} |
| 290 | + |
| 291 | +// ConvertToVolumes converts the VolumeConfigs map on a libcompose project into |
| 292 | +// a Volumes struct and populates the VolumeEmptyHost field with any named volumes |
| 293 | +func ConvertToVolumes(volumeConfigs map[string]*config.VolumeConfig) (*Volumes, error) { |
| 294 | + volumes := &Volumes{ |
| 295 | + VolumeWithHost: make(map[string]string), // map with key:=hostSourcePath value:=VolumeName |
| 296 | + } |
| 297 | + |
| 298 | + // Add named volume configs: |
| 299 | + if volumeConfigs != nil { |
| 300 | + for name, config := range volumeConfigs { |
| 301 | + if config != nil { |
| 302 | + // NOTE: If Driver field is not empty, this |
| 303 | + // will add a prefix to the named volume on the container |
| 304 | + if config.Driver != "" { |
| 305 | + return nil, errors.New("Volume driver is not supported") |
| 306 | + } |
| 307 | + // Driver Options must relate to a specific volume driver |
| 308 | + if len(config.DriverOpts) != 0 { |
| 309 | + return nil, errors.New("Volume driver options is not supported") |
| 310 | + } |
| 311 | + return nil, errors.New("External option is not supported") |
| 312 | + } |
| 313 | + volumes.VolumeEmptyHost = append(volumes.VolumeEmptyHost, name) |
| 314 | + } |
| 315 | + } |
| 316 | + |
| 317 | + return volumes, nil |
| 318 | +} |
| 319 | + |
| 320 | +// ConvertToVolumesFrom transforms the yml volumes from to ecs compatible VolumesFrom slice |
| 321 | +// Examples for compose format v2: |
| 322 | +// volumes_from: |
| 323 | +// - service_name |
| 324 | +// - service_name:ro |
| 325 | +// - container:container_name |
| 326 | +// - container:container_name:rw |
| 327 | +// Examples for compose format v1: |
| 328 | +// volumes_from: |
| 329 | +// - service_name |
| 330 | +// - service_name:ro |
| 331 | +// - container_name |
| 332 | +// - container_name:rw |
| 333 | +func ConvertToVolumesFrom(cfgVolumesFrom []string) ([]*ecs.VolumeFrom, error) { |
| 334 | + volumesFrom := []*ecs.VolumeFrom{} |
| 335 | + |
| 336 | + for _, cfgVolumeFrom := range cfgVolumesFrom { |
| 337 | + parts := strings.Split(cfgVolumeFrom, ":") |
| 338 | + |
| 339 | + var containerName, accessModeStr string |
| 340 | + |
| 341 | + parseErr := fmt.Errorf( |
| 342 | + "expected format [container:]SERVICE|CONTAINER[:ro|rw]. could not parse cfgVolumeFrom: %s", cfgVolumeFrom) |
| 343 | + |
| 344 | + switch len(parts) { |
| 345 | + // for the following volumes_from formats (supported by compose file formats v1 and v2), |
| 346 | + // name: refers to either service_name or container_name |
| 347 | + // container: is a keyword thats introduced in v2 to differentiate between service_name and container:container_name |
| 348 | + // ro|rw: read-only or read-write access |
| 349 | + case 1: // Format: name |
| 350 | + containerName = parts[0] |
| 351 | + case 2: // Format: name:ro|rw (OR) container:name |
| 352 | + if parts[0] == volumeFromContainerKey { |
| 353 | + containerName = parts[1] |
| 354 | + } else { |
| 355 | + containerName = parts[0] |
| 356 | + accessModeStr = parts[1] |
| 357 | + } |
| 358 | + case 3: // Format: container:name:ro|rw |
| 359 | + if parts[0] != volumeFromContainerKey { |
| 360 | + return nil, parseErr |
| 361 | + } |
| 362 | + containerName = parts[1] |
| 363 | + accessModeStr = parts[2] |
| 364 | + default: |
| 365 | + return nil, parseErr |
| 366 | + } |
| 367 | + |
| 368 | + // parse accessModeStr |
| 369 | + var readOnly bool |
| 370 | + if accessModeStr != "" { |
| 371 | + if accessModeStr == readOnlyVolumeAccessMode { |
| 372 | + readOnly = true |
| 373 | + } else if accessModeStr == readWriteVolumeAccessMode { |
| 374 | + readOnly = false |
| 375 | + } else { |
| 376 | + return nil, fmt.Errorf("Could not parse access mode %s", accessModeStr) |
| 377 | + } |
| 378 | + } |
| 379 | + volumesFrom = append(volumesFrom, &ecs.VolumeFrom{ |
| 380 | + SourceContainer: aws.String(containerName), |
| 381 | + ReadOnly: aws.Bool(readOnly), |
| 382 | + }) |
| 383 | + } |
| 384 | + return volumesFrom, nil |
| 385 | +} |
| 386 | + |
| 387 | +// SortedGoString returns deterministic string representation |
| 388 | +// json Marshal sorts map keys, making it deterministic |
| 389 | +func SortedGoString(v interface{}) (string, error) { |
| 390 | + b, err := json.Marshal(v) |
| 391 | + if err != nil { |
| 392 | + return "", err |
| 393 | + } |
| 394 | + return string(b), nil |
| 395 | +} |
0 commit comments