Skip to content

Commit a3af916

Browse files
SoManyHsallisaurus
authored andcommitted
Move private conversion methods to adapter
1 parent 346d885 commit a3af916

File tree

11 files changed

+925
-882
lines changed

11 files changed

+925
-882
lines changed
Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
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

Comments
 (0)