Browse code

Replace vendor of aanand/compose-file with a local copy.

Add go-bindata for including the schema.

Signed-off-by: Daniel Nephin <dnephin@docker.com>

Daniel Nephin authored on 2016/12/21 06:26:49
Showing 33 changed files
... ...
@@ -237,7 +237,7 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \
237 237
 # Please edit hack/dockerfile/install-binaries.sh to update them.
238 238
 COPY hack/dockerfile/binaries-commits /tmp/binaries-commits
239 239
 COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh
240
-RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy
240
+RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy bindata
241 241
 
242 242
 # Wrap all commands in the "docker-in-docker" script to allow nested containers
243 243
 ENTRYPOINT ["hack/dind"]
... ...
@@ -8,13 +8,13 @@ import (
8 8
 	"sort"
9 9
 	"strings"
10 10
 
11
-	"github.com/aanand/compose-file/loader"
12
-	composetypes "github.com/aanand/compose-file/types"
13 11
 	"github.com/docker/docker/api/types"
14 12
 	"github.com/docker/docker/api/types/swarm"
15 13
 	"github.com/docker/docker/cli"
16 14
 	"github.com/docker/docker/cli/command"
17 15
 	"github.com/docker/docker/cli/compose/convert"
16
+	"github.com/docker/docker/cli/compose/loader"
17
+	composetypes "github.com/docker/docker/cli/compose/types"
18 18
 	dockerclient "github.com/docker/docker/client"
19 19
 	"github.com/spf13/cobra"
20 20
 	"golang.org/x/net/context"
... ...
@@ -1,9 +1,9 @@
1 1
 package convert
2 2
 
3 3
 import (
4
-	composetypes "github.com/aanand/compose-file/types"
5 4
 	"github.com/docker/docker/api/types"
6 5
 	networktypes "github.com/docker/docker/api/types/network"
6
+	composetypes "github.com/docker/docker/cli/compose/types"
7 7
 )
8 8
 
9 9
 const (
... ...
@@ -3,9 +3,9 @@ package convert
3 3
 import (
4 4
 	"testing"
5 5
 
6
-	composetypes "github.com/aanand/compose-file/types"
7 6
 	"github.com/docker/docker/api/types"
8 7
 	"github.com/docker/docker/api/types/network"
8
+	composetypes "github.com/docker/docker/cli/compose/types"
9 9
 	"github.com/docker/docker/pkg/testutil/assert"
10 10
 )
11 11
 
... ...
@@ -4,9 +4,9 @@ import (
4 4
 	"fmt"
5 5
 	"time"
6 6
 
7
-	composetypes "github.com/aanand/compose-file/types"
8 7
 	"github.com/docker/docker/api/types/container"
9 8
 	"github.com/docker/docker/api/types/swarm"
9
+	composetypes "github.com/docker/docker/cli/compose/types"
10 10
 	"github.com/docker/docker/opts"
11 11
 	runconfigopts "github.com/docker/docker/runconfig/opts"
12 12
 	"github.com/docker/go-connections/nat"
... ...
@@ -6,9 +6,9 @@ import (
6 6
 	"testing"
7 7
 	"time"
8 8
 
9
-	composetypes "github.com/aanand/compose-file/types"
10 9
 	"github.com/docker/docker/api/types/container"
11 10
 	"github.com/docker/docker/api/types/swarm"
11
+	composetypes "github.com/docker/docker/cli/compose/types"
12 12
 	"github.com/docker/docker/pkg/testutil/assert"
13 13
 )
14 14
 
... ...
@@ -4,8 +4,8 @@ import (
4 4
 	"fmt"
5 5
 	"strings"
6 6
 
7
-	composetypes "github.com/aanand/compose-file/types"
8 7
 	"github.com/docker/docker/api/types/mount"
8
+	composetypes "github.com/docker/docker/cli/compose/types"
9 9
 )
10 10
 
11 11
 type volumes map[string]composetypes.VolumeConfig
... ...
@@ -3,8 +3,8 @@ package convert
3 3
 import (
4 4
 	"testing"
5 5
 
6
-	composetypes "github.com/aanand/compose-file/types"
7 6
 	"github.com/docker/docker/api/types/mount"
7
+	composetypes "github.com/docker/docker/cli/compose/types"
8 8
 	"github.com/docker/docker/pkg/testutil/assert"
9 9
 )
10 10
 
11 11
new file mode 100644
... ...
@@ -0,0 +1,90 @@
0
+package interpolation
1
+
2
+import (
3
+	"fmt"
4
+
5
+	"github.com/docker/docker/cli/compose/template"
6
+	"github.com/docker/docker/cli/compose/types"
7
+)
8
+
9
+// Interpolate replaces variables in a string with the values from a mapping
10
+func Interpolate(config types.Dict, section string, mapping template.Mapping) (types.Dict, error) {
11
+	out := types.Dict{}
12
+
13
+	for name, item := range config {
14
+		if item == nil {
15
+			out[name] = nil
16
+			continue
17
+		}
18
+		interpolatedItem, err := interpolateSectionItem(name, item.(types.Dict), section, mapping)
19
+		if err != nil {
20
+			return nil, err
21
+		}
22
+		out[name] = interpolatedItem
23
+	}
24
+
25
+	return out, nil
26
+}
27
+
28
+func interpolateSectionItem(
29
+	name string,
30
+	item types.Dict,
31
+	section string,
32
+	mapping template.Mapping,
33
+) (types.Dict, error) {
34
+
35
+	out := types.Dict{}
36
+
37
+	for key, value := range item {
38
+		interpolatedValue, err := recursiveInterpolate(value, mapping)
39
+		if err != nil {
40
+			return nil, fmt.Errorf(
41
+				"Invalid interpolation format for %#v option in %s %#v: %#v",
42
+				key, section, name, err.Template,
43
+			)
44
+		}
45
+		out[key] = interpolatedValue
46
+	}
47
+
48
+	return out, nil
49
+
50
+}
51
+
52
+func recursiveInterpolate(
53
+	value interface{},
54
+	mapping template.Mapping,
55
+) (interface{}, *template.InvalidTemplateError) {
56
+
57
+	switch value := value.(type) {
58
+
59
+	case string:
60
+		return template.Substitute(value, mapping)
61
+
62
+	case types.Dict:
63
+		out := types.Dict{}
64
+		for key, elem := range value {
65
+			interpolatedElem, err := recursiveInterpolate(elem, mapping)
66
+			if err != nil {
67
+				return nil, err
68
+			}
69
+			out[key] = interpolatedElem
70
+		}
71
+		return out, nil
72
+
73
+	case []interface{}:
74
+		out := make([]interface{}, len(value))
75
+		for i, elem := range value {
76
+			interpolatedElem, err := recursiveInterpolate(elem, mapping)
77
+			if err != nil {
78
+				return nil, err
79
+			}
80
+			out[i] = interpolatedElem
81
+		}
82
+		return out, nil
83
+
84
+	default:
85
+		return value, nil
86
+
87
+	}
88
+
89
+}
0 90
new file mode 100644
... ...
@@ -0,0 +1,59 @@
0
+package interpolation
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/stretchr/testify/assert"
6
+
7
+	"github.com/docker/docker/cli/compose/types"
8
+)
9
+
10
+var defaults = map[string]string{
11
+	"USER": "jenny",
12
+	"FOO":  "bar",
13
+}
14
+
15
+func defaultMapping(name string) (string, bool) {
16
+	val, ok := defaults[name]
17
+	return val, ok
18
+}
19
+
20
+func TestInterpolate(t *testing.T) {
21
+	services := types.Dict{
22
+		"servicea": types.Dict{
23
+			"image":   "example:${USER}",
24
+			"volumes": []interface{}{"$FOO:/target"},
25
+			"logging": types.Dict{
26
+				"driver": "${FOO}",
27
+				"options": types.Dict{
28
+					"user": "$USER",
29
+				},
30
+			},
31
+		},
32
+	}
33
+	expected := types.Dict{
34
+		"servicea": types.Dict{
35
+			"image":   "example:jenny",
36
+			"volumes": []interface{}{"bar:/target"},
37
+			"logging": types.Dict{
38
+				"driver": "bar",
39
+				"options": types.Dict{
40
+					"user": "jenny",
41
+				},
42
+			},
43
+		},
44
+	}
45
+	result, err := Interpolate(services, "service", defaultMapping)
46
+	assert.NoError(t, err)
47
+	assert.Equal(t, expected, result)
48
+}
49
+
50
+func TestInvalidInterpolation(t *testing.T) {
51
+	services := types.Dict{
52
+		"servicea": types.Dict{
53
+			"image": "${",
54
+		},
55
+	}
56
+	_, err := Interpolate(services, "service", defaultMapping)
57
+	assert.EqualError(t, err, `Invalid interpolation format for "image" option in service "servicea": "${"`)
58
+}
0 59
new file mode 100644
... ...
@@ -0,0 +1,8 @@
0
+# passed through
1
+FOO=1
2
+
3
+# overridden in example2.env
4
+BAR=1
5
+
6
+# overridden in full-example.yml
7
+BAZ=1
0 8
new file mode 100644
... ...
@@ -0,0 +1 @@
0
+BAR=2
0 1
new file mode 100644
... ...
@@ -0,0 +1,287 @@
0
+version: "3"
1
+
2
+services:
3
+  foo:
4
+    cap_add:
5
+      - ALL
6
+
7
+    cap_drop:
8
+      - NET_ADMIN
9
+      - SYS_ADMIN
10
+
11
+    cgroup_parent: m-executor-abcd
12
+
13
+    # String or list
14
+    command: bundle exec thin -p 3000
15
+    # command: ["bundle", "exec", "thin", "-p", "3000"]
16
+
17
+    container_name: my-web-container
18
+
19
+    depends_on:
20
+      - db
21
+      - redis
22
+
23
+    deploy:
24
+      mode: replicated
25
+      replicas: 6
26
+      labels: [FOO=BAR]
27
+      update_config:
28
+        parallelism: 3
29
+        delay: 10s
30
+        failure_action: continue
31
+        monitor: 60s
32
+        max_failure_ratio: 0.3
33
+      resources:
34
+        limits:
35
+          cpus: '0.001'
36
+          memory: 50M
37
+        reservations:
38
+          cpus: '0.0001'
39
+          memory: 20M
40
+      restart_policy:
41
+        condition: on_failure
42
+        delay: 5s
43
+        max_attempts: 3
44
+        window: 120s
45
+      placement:
46
+        constraints: [node=foo]
47
+
48
+    devices:
49
+      - "/dev/ttyUSB0:/dev/ttyUSB0"
50
+
51
+    # String or list
52
+    # dns: 8.8.8.8
53
+    dns:
54
+      - 8.8.8.8
55
+      - 9.9.9.9
56
+
57
+    # String or list
58
+    # dns_search: example.com
59
+    dns_search:
60
+      - dc1.example.com
61
+      - dc2.example.com
62
+
63
+    domainname: foo.com
64
+
65
+    # String or list
66
+    # entrypoint: /code/entrypoint.sh -p 3000
67
+    entrypoint: ["/code/entrypoint.sh", "-p", "3000"]
68
+
69
+    # String or list
70
+    # env_file: .env
71
+    env_file:
72
+      - ./example1.env
73
+      - ./example2.env
74
+
75
+    # Mapping or list
76
+    # Mapping values can be strings, numbers or null
77
+    # Booleans are not allowed - must be quoted
78
+    environment:
79
+      RACK_ENV: development
80
+      SHOW: 'true'
81
+      SESSION_SECRET:
82
+      BAZ: 3
83
+    # environment:
84
+    #   - RACK_ENV=development
85
+    #   - SHOW=true
86
+    #   - SESSION_SECRET
87
+
88
+    # Items can be strings or numbers
89
+    expose:
90
+     - "3000"
91
+     - 8000
92
+
93
+    external_links:
94
+      - redis_1
95
+      - project_db_1:mysql
96
+      - project_db_1:postgresql
97
+
98
+    # Mapping or list
99
+    # Mapping values must be strings
100
+    # extra_hosts:
101
+    #   somehost: "162.242.195.82"
102
+    #   otherhost: "50.31.209.229"
103
+    extra_hosts:
104
+      - "somehost:162.242.195.82"
105
+      - "otherhost:50.31.209.229"
106
+
107
+    hostname: foo
108
+
109
+    healthcheck:
110
+      test: echo "hello world"
111
+      interval: 10s
112
+      timeout: 1s
113
+      retries: 5
114
+
115
+    # Any valid image reference - repo, tag, id, sha
116
+    image: redis
117
+    # image: ubuntu:14.04
118
+    # image: tutum/influxdb
119
+    # image: example-registry.com:4000/postgresql
120
+    # image: a4bc65fd
121
+    # image: busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d
122
+
123
+    ipc: host
124
+
125
+    # Mapping or list
126
+    # Mapping values can be strings, numbers or null
127
+    labels:
128
+      com.example.description: "Accounting webapp"
129
+      com.example.number: 42
130
+      com.example.empty-label:
131
+    # labels:
132
+    #   - "com.example.description=Accounting webapp"
133
+    #   - "com.example.number=42"
134
+    #   - "com.example.empty-label"
135
+
136
+    links:
137
+     - db
138
+     - db:database
139
+     - redis
140
+
141
+    logging:
142
+      driver: syslog
143
+      options:
144
+        syslog-address: "tcp://192.168.0.42:123"
145
+
146
+    mac_address: 02:42:ac:11:65:43
147
+
148
+    # network_mode: "bridge"
149
+    # network_mode: "host"
150
+    # network_mode: "none"
151
+    # Use the network mode of an arbitrary container from another service
152
+    # network_mode: "service:db"
153
+    # Use the network mode of another container, specified by name or id
154
+    # network_mode: "container:some-container"
155
+    network_mode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b"
156
+
157
+    networks:
158
+      some-network:
159
+        aliases:
160
+         - alias1
161
+         - alias3
162
+      other-network:
163
+        ipv4_address: 172.16.238.10
164
+        ipv6_address: 2001:3984:3989::10
165
+      other-other-network:
166
+
167
+    pid: "host"
168
+
169
+    ports:
170
+      - 3000
171
+      - "3000-3005"
172
+      - "8000:8000"
173
+      - "9090-9091:8080-8081"
174
+      - "49100:22"
175
+      - "127.0.0.1:8001:8001"
176
+      - "127.0.0.1:5000-5010:5000-5010"
177
+
178
+    privileged: true
179
+
180
+    read_only: true
181
+
182
+    restart: always
183
+
184
+    security_opt:
185
+      - label=level:s0:c100,c200
186
+      - label=type:svirt_apache_t
187
+
188
+    stdin_open: true
189
+
190
+    stop_grace_period: 20s
191
+
192
+    stop_signal: SIGUSR1
193
+
194
+    # String or list
195
+    # tmpfs: /run
196
+    tmpfs:
197
+      - /run
198
+      - /tmp
199
+
200
+    tty: true
201
+
202
+    ulimits:
203
+      # Single number or mapping with soft + hard limits
204
+      nproc: 65535
205
+      nofile:
206
+        soft: 20000
207
+        hard: 40000
208
+
209
+    user: someone
210
+
211
+    volumes:
212
+      # Just specify a path and let the Engine create a volume
213
+      - /var/lib/mysql
214
+      # Specify an absolute path mapping
215
+      - /opt/data:/var/lib/mysql
216
+      # Path on the host, relative to the Compose file
217
+      - .:/code
218
+      - ./static:/var/www/html
219
+      # User-relative path
220
+      - ~/configs:/etc/configs/:ro
221
+      # Named volume
222
+      - datavolume:/var/lib/mysql
223
+
224
+    working_dir: /code
225
+
226
+networks:
227
+  # Entries can be null, which specifies simply that a network
228
+  # called "{project name}_some-network" should be created and
229
+  # use the default driver
230
+  some-network:
231
+
232
+  other-network:
233
+    driver: overlay
234
+
235
+    driver_opts:
236
+      # Values can be strings or numbers
237
+      foo: "bar"
238
+      baz: 1
239
+
240
+    ipam:
241
+      driver: overlay
242
+      # driver_opts:
243
+      #   # Values can be strings or numbers
244
+      #   com.docker.network.enable_ipv6: "true"
245
+      #   com.docker.network.numeric_value: 1
246
+      config:
247
+      - subnet: 172.16.238.0/24
248
+        # gateway: 172.16.238.1
249
+      - subnet: 2001:3984:3989::/64
250
+        # gateway: 2001:3984:3989::1
251
+
252
+  external-network:
253
+    # Specifies that a pre-existing network called "external-network"
254
+    # can be referred to within this file as "external-network"
255
+    external: true
256
+
257
+  other-external-network:
258
+    # Specifies that a pre-existing network called "my-cool-network"
259
+    # can be referred to within this file as "other-external-network"
260
+    external:
261
+      name: my-cool-network
262
+
263
+volumes:
264
+  # Entries can be null, which specifies simply that a volume
265
+  # called "{project name}_some-volume" should be created and
266
+  # use the default driver
267
+  some-volume:
268
+
269
+  other-volume:
270
+    driver: flocker
271
+
272
+    driver_opts:
273
+      # Values can be strings or numbers
274
+      foo: "bar"
275
+      baz: 1
276
+
277
+  external-volume:
278
+    # Specifies that a pre-existing volume called "external-volume"
279
+    # can be referred to within this file as "external-volume"
280
+    external: true
281
+
282
+  other-external-volume:
283
+    # Specifies that a pre-existing volume called "my-cool-volume"
284
+    # can be referred to within this file as "other-external-volume"
285
+    external:
286
+      name: my-cool-volume
0 287
new file mode 100644
... ...
@@ -0,0 +1,611 @@
0
+package loader
1
+
2
+import (
3
+	"fmt"
4
+	"os"
5
+	"path"
6
+	"reflect"
7
+	"regexp"
8
+	"sort"
9
+	"strings"
10
+
11
+	"github.com/docker/docker/cli/compose/interpolation"
12
+	"github.com/docker/docker/cli/compose/schema"
13
+	"github.com/docker/docker/cli/compose/types"
14
+	"github.com/docker/docker/runconfig/opts"
15
+	units "github.com/docker/go-units"
16
+	shellwords "github.com/mattn/go-shellwords"
17
+	"github.com/mitchellh/mapstructure"
18
+	yaml "gopkg.in/yaml.v2"
19
+)
20
+
21
+var (
22
+	fieldNameRegexp = regexp.MustCompile("[A-Z][a-z0-9]+")
23
+)
24
+
25
+// ParseYAML reads the bytes from a file, parses the bytes into a mapping
26
+// structure, and returns it.
27
+func ParseYAML(source []byte) (types.Dict, error) {
28
+	var cfg interface{}
29
+	if err := yaml.Unmarshal(source, &cfg); err != nil {
30
+		return nil, err
31
+	}
32
+	cfgMap, ok := cfg.(map[interface{}]interface{})
33
+	if !ok {
34
+		return nil, fmt.Errorf("Top-level object must be a mapping")
35
+	}
36
+	converted, err := convertToStringKeysRecursive(cfgMap, "")
37
+	if err != nil {
38
+		return nil, err
39
+	}
40
+	return converted.(types.Dict), nil
41
+}
42
+
43
+// Load reads a ConfigDetails and returns a fully loaded configuration
44
+func Load(configDetails types.ConfigDetails) (*types.Config, error) {
45
+	if len(configDetails.ConfigFiles) < 1 {
46
+		return nil, fmt.Errorf("No files specified")
47
+	}
48
+	if len(configDetails.ConfigFiles) > 1 {
49
+		return nil, fmt.Errorf("Multiple files are not yet supported")
50
+	}
51
+
52
+	configDict := getConfigDict(configDetails)
53
+
54
+	if services, ok := configDict["services"]; ok {
55
+		if servicesDict, ok := services.(types.Dict); ok {
56
+			forbidden := getProperties(servicesDict, types.ForbiddenProperties)
57
+
58
+			if len(forbidden) > 0 {
59
+				return nil, &ForbiddenPropertiesError{Properties: forbidden}
60
+			}
61
+		}
62
+	}
63
+
64
+	if err := schema.Validate(configDict); err != nil {
65
+		return nil, err
66
+	}
67
+
68
+	cfg := types.Config{}
69
+	version := configDict["version"].(string)
70
+	if version != "3" && version != "3.0" {
71
+		return nil, fmt.Errorf(`Unsupported Compose file version: %#v. The only version supported is "3" (or "3.0")`, version)
72
+	}
73
+
74
+	if services, ok := configDict["services"]; ok {
75
+		servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", os.LookupEnv)
76
+		if err != nil {
77
+			return nil, err
78
+		}
79
+
80
+		servicesList, err := loadServices(servicesConfig, configDetails.WorkingDir)
81
+		if err != nil {
82
+			return nil, err
83
+		}
84
+
85
+		cfg.Services = servicesList
86
+	}
87
+
88
+	if networks, ok := configDict["networks"]; ok {
89
+		networksConfig, err := interpolation.Interpolate(networks.(types.Dict), "network", os.LookupEnv)
90
+		if err != nil {
91
+			return nil, err
92
+		}
93
+
94
+		networksMapping, err := loadNetworks(networksConfig)
95
+		if err != nil {
96
+			return nil, err
97
+		}
98
+
99
+		cfg.Networks = networksMapping
100
+	}
101
+
102
+	if volumes, ok := configDict["volumes"]; ok {
103
+		volumesConfig, err := interpolation.Interpolate(volumes.(types.Dict), "volume", os.LookupEnv)
104
+		if err != nil {
105
+			return nil, err
106
+		}
107
+
108
+		volumesMapping, err := loadVolumes(volumesConfig)
109
+		if err != nil {
110
+			return nil, err
111
+		}
112
+
113
+		cfg.Volumes = volumesMapping
114
+	}
115
+
116
+	return &cfg, nil
117
+}
118
+
119
+// GetUnsupportedProperties returns the list of any unsupported properties that are
120
+// used in the Compose files.
121
+func GetUnsupportedProperties(configDetails types.ConfigDetails) []string {
122
+	unsupported := map[string]bool{}
123
+
124
+	for _, service := range getServices(getConfigDict(configDetails)) {
125
+		serviceDict := service.(types.Dict)
126
+		for _, property := range types.UnsupportedProperties {
127
+			if _, isSet := serviceDict[property]; isSet {
128
+				unsupported[property] = true
129
+			}
130
+		}
131
+	}
132
+
133
+	return sortedKeys(unsupported)
134
+}
135
+
136
+func sortedKeys(set map[string]bool) []string {
137
+	var keys []string
138
+	for key := range set {
139
+		keys = append(keys, key)
140
+	}
141
+	sort.Strings(keys)
142
+	return keys
143
+}
144
+
145
+// GetDeprecatedProperties returns the list of any deprecated properties that
146
+// are used in the compose files.
147
+func GetDeprecatedProperties(configDetails types.ConfigDetails) map[string]string {
148
+	return getProperties(getServices(getConfigDict(configDetails)), types.DeprecatedProperties)
149
+}
150
+
151
+func getProperties(services types.Dict, propertyMap map[string]string) map[string]string {
152
+	output := map[string]string{}
153
+
154
+	for _, service := range services {
155
+		if serviceDict, ok := service.(types.Dict); ok {
156
+			for property, description := range propertyMap {
157
+				if _, isSet := serviceDict[property]; isSet {
158
+					output[property] = description
159
+				}
160
+			}
161
+		}
162
+	}
163
+
164
+	return output
165
+}
166
+
167
+// ForbiddenPropertiesError is returned when there are properties in the Compose
168
+// file that are forbidden.
169
+type ForbiddenPropertiesError struct {
170
+	Properties map[string]string
171
+}
172
+
173
+func (e *ForbiddenPropertiesError) Error() string {
174
+	return "Configuration contains forbidden properties"
175
+}
176
+
177
+// TODO: resolve multiple files into a single config
178
+func getConfigDict(configDetails types.ConfigDetails) types.Dict {
179
+	return configDetails.ConfigFiles[0].Config
180
+}
181
+
182
+func getServices(configDict types.Dict) types.Dict {
183
+	if services, ok := configDict["services"]; ok {
184
+		if servicesDict, ok := services.(types.Dict); ok {
185
+			return servicesDict
186
+		}
187
+	}
188
+
189
+	return types.Dict{}
190
+}
191
+
192
+func transform(source map[string]interface{}, target interface{}) error {
193
+	data := mapstructure.Metadata{}
194
+	config := &mapstructure.DecoderConfig{
195
+		DecodeHook: mapstructure.ComposeDecodeHookFunc(
196
+			transformHook,
197
+			mapstructure.StringToTimeDurationHookFunc()),
198
+		Result:   target,
199
+		Metadata: &data,
200
+	}
201
+	decoder, err := mapstructure.NewDecoder(config)
202
+	if err != nil {
203
+		return err
204
+	}
205
+	err = decoder.Decode(source)
206
+	// TODO: log unused keys
207
+	return err
208
+}
209
+
210
+func transformHook(
211
+	source reflect.Type,
212
+	target reflect.Type,
213
+	data interface{},
214
+) (interface{}, error) {
215
+	switch target {
216
+	case reflect.TypeOf(types.External{}):
217
+		return transformExternal(source, target, data)
218
+	case reflect.TypeOf(make(map[string]string, 0)):
219
+		return transformMapStringString(source, target, data)
220
+	case reflect.TypeOf(types.UlimitsConfig{}):
221
+		return transformUlimits(source, target, data)
222
+	case reflect.TypeOf(types.UnitBytes(0)):
223
+		return loadSize(data)
224
+	}
225
+	switch target.Kind() {
226
+	case reflect.Struct:
227
+		return transformStruct(source, target, data)
228
+	}
229
+	return data, nil
230
+}
231
+
232
+// keys needs to be converted to strings for jsonschema
233
+// TODO: don't use types.Dict
234
+func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) {
235
+	if mapping, ok := value.(map[interface{}]interface{}); ok {
236
+		dict := make(types.Dict)
237
+		for key, entry := range mapping {
238
+			str, ok := key.(string)
239
+			if !ok {
240
+				var location string
241
+				if keyPrefix == "" {
242
+					location = "at top level"
243
+				} else {
244
+					location = fmt.Sprintf("in %s", keyPrefix)
245
+				}
246
+				return nil, fmt.Errorf("Non-string key %s: %#v", location, key)
247
+			}
248
+			var newKeyPrefix string
249
+			if keyPrefix == "" {
250
+				newKeyPrefix = str
251
+			} else {
252
+				newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str)
253
+			}
254
+			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
255
+			if err != nil {
256
+				return nil, err
257
+			}
258
+			dict[str] = convertedEntry
259
+		}
260
+		return dict, nil
261
+	}
262
+	if list, ok := value.([]interface{}); ok {
263
+		var convertedList []interface{}
264
+		for index, entry := range list {
265
+			newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index)
266
+			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
267
+			if err != nil {
268
+				return nil, err
269
+			}
270
+			convertedList = append(convertedList, convertedEntry)
271
+		}
272
+		return convertedList, nil
273
+	}
274
+	return value, nil
275
+}
276
+
277
+func loadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceConfig, error) {
278
+	var services []types.ServiceConfig
279
+
280
+	for name, serviceDef := range servicesDict {
281
+		serviceConfig, err := loadService(name, serviceDef.(types.Dict), workingDir)
282
+		if err != nil {
283
+			return nil, err
284
+		}
285
+		services = append(services, *serviceConfig)
286
+	}
287
+
288
+	return services, nil
289
+}
290
+
291
+func loadService(name string, serviceDict types.Dict, workingDir string) (*types.ServiceConfig, error) {
292
+	serviceConfig := &types.ServiceConfig{}
293
+	if err := transform(serviceDict, serviceConfig); err != nil {
294
+		return nil, err
295
+	}
296
+	serviceConfig.Name = name
297
+
298
+	if err := resolveEnvironment(serviceConfig, serviceDict, workingDir); err != nil {
299
+		return nil, err
300
+	}
301
+
302
+	if err := resolveVolumePaths(serviceConfig.Volumes, workingDir); err != nil {
303
+		return nil, err
304
+	}
305
+
306
+	return serviceConfig, nil
307
+}
308
+
309
+func resolveEnvironment(serviceConfig *types.ServiceConfig, serviceDict types.Dict, workingDir string) error {
310
+	environment := make(map[string]string)
311
+
312
+	if envFileVal, ok := serviceDict["env_file"]; ok {
313
+		envFiles := loadStringOrListOfStrings(envFileVal)
314
+
315
+		var envVars []string
316
+
317
+		for _, file := range envFiles {
318
+			filePath := path.Join(workingDir, file)
319
+			fileVars, err := opts.ParseEnvFile(filePath)
320
+			if err != nil {
321
+				return err
322
+			}
323
+			envVars = append(envVars, fileVars...)
324
+		}
325
+
326
+		for k, v := range opts.ConvertKVStringsToMap(envVars) {
327
+			environment[k] = v
328
+		}
329
+	}
330
+
331
+	for k, v := range serviceConfig.Environment {
332
+		environment[k] = v
333
+	}
334
+
335
+	serviceConfig.Environment = environment
336
+
337
+	return nil
338
+}
339
+
340
+func resolveVolumePaths(volumes []string, workingDir string) error {
341
+	for i, mapping := range volumes {
342
+		parts := strings.SplitN(mapping, ":", 2)
343
+		if len(parts) == 1 {
344
+			continue
345
+		}
346
+
347
+		if strings.HasPrefix(parts[0], ".") {
348
+			parts[0] = path.Join(workingDir, parts[0])
349
+		}
350
+		parts[0] = expandUser(parts[0])
351
+
352
+		volumes[i] = strings.Join(parts, ":")
353
+	}
354
+
355
+	return nil
356
+}
357
+
358
+// TODO: make this more robust
359
+func expandUser(path string) string {
360
+	if strings.HasPrefix(path, "~") {
361
+		return strings.Replace(path, "~", os.Getenv("HOME"), 1)
362
+	}
363
+	return path
364
+}
365
+
366
+func transformUlimits(
367
+	source reflect.Type,
368
+	target reflect.Type,
369
+	data interface{},
370
+) (interface{}, error) {
371
+	switch value := data.(type) {
372
+	case int:
373
+		return types.UlimitsConfig{Single: value}, nil
374
+	case types.Dict:
375
+		ulimit := types.UlimitsConfig{}
376
+		ulimit.Soft = value["soft"].(int)
377
+		ulimit.Hard = value["hard"].(int)
378
+		return ulimit, nil
379
+	default:
380
+		return data, fmt.Errorf("invalid type %T for ulimits", value)
381
+	}
382
+}
383
+
384
+func loadNetworks(source types.Dict) (map[string]types.NetworkConfig, error) {
385
+	networks := make(map[string]types.NetworkConfig)
386
+	err := transform(source, &networks)
387
+	if err != nil {
388
+		return networks, err
389
+	}
390
+	for name, network := range networks {
391
+		if network.External.External && network.External.Name == "" {
392
+			network.External.Name = name
393
+			networks[name] = network
394
+		}
395
+	}
396
+	return networks, nil
397
+}
398
+
399
+func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) {
400
+	volumes := make(map[string]types.VolumeConfig)
401
+	err := transform(source, &volumes)
402
+	if err != nil {
403
+		return volumes, err
404
+	}
405
+	for name, volume := range volumes {
406
+		if volume.External.External && volume.External.Name == "" {
407
+			volume.External.Name = name
408
+			volumes[name] = volume
409
+		}
410
+	}
411
+	return volumes, nil
412
+}
413
+
414
+func transformStruct(
415
+	source reflect.Type,
416
+	target reflect.Type,
417
+	data interface{},
418
+) (interface{}, error) {
419
+	structValue, ok := data.(map[string]interface{})
420
+	if !ok {
421
+		// FIXME: this is necessary because of convertToStringKeysRecursive
422
+		structValue, ok = data.(types.Dict)
423
+		if !ok {
424
+			panic(fmt.Sprintf(
425
+				"transformStruct called with non-map type: %T, %s", data, data))
426
+		}
427
+	}
428
+
429
+	var err error
430
+	for i := 0; i < target.NumField(); i++ {
431
+		field := target.Field(i)
432
+		fieldTag := field.Tag.Get("compose")
433
+
434
+		yamlName := toYAMLName(field.Name)
435
+		value, ok := structValue[yamlName]
436
+		if !ok {
437
+			continue
438
+		}
439
+
440
+		structValue[yamlName], err = convertField(
441
+			fieldTag, reflect.TypeOf(value), field.Type, value)
442
+		if err != nil {
443
+			return nil, fmt.Errorf("field %s: %s", yamlName, err.Error())
444
+		}
445
+	}
446
+	return structValue, nil
447
+}
448
+
449
+func transformMapStringString(
450
+	source reflect.Type,
451
+	target reflect.Type,
452
+	data interface{},
453
+) (interface{}, error) {
454
+	switch value := data.(type) {
455
+	case map[string]interface{}:
456
+		return toMapStringString(value), nil
457
+	case types.Dict:
458
+		return toMapStringString(value), nil
459
+	case map[string]string:
460
+		return value, nil
461
+	default:
462
+		return data, fmt.Errorf("invalid type %T for map[string]string", value)
463
+	}
464
+}
465
+
466
+func convertField(
467
+	fieldTag string,
468
+	source reflect.Type,
469
+	target reflect.Type,
470
+	data interface{},
471
+) (interface{}, error) {
472
+	switch fieldTag {
473
+	case "":
474
+		return data, nil
475
+	case "healthcheck":
476
+		return loadHealthcheck(data)
477
+	case "list_or_dict_equals":
478
+		return loadMappingOrList(data, "="), nil
479
+	case "list_or_dict_colon":
480
+		return loadMappingOrList(data, ":"), nil
481
+	case "list_or_struct_map":
482
+		return loadListOrStructMap(data, target)
483
+	case "string_or_list":
484
+		return loadStringOrListOfStrings(data), nil
485
+	case "list_of_strings_or_numbers":
486
+		return loadListOfStringsOrNumbers(data), nil
487
+	case "shell_command":
488
+		return loadShellCommand(data)
489
+	case "size":
490
+		return loadSize(data)
491
+	case "-":
492
+		return nil, nil
493
+	}
494
+	return data, nil
495
+}
496
+
497
+func transformExternal(
498
+	source reflect.Type,
499
+	target reflect.Type,
500
+	data interface{},
501
+) (interface{}, error) {
502
+	switch value := data.(type) {
503
+	case bool:
504
+		return map[string]interface{}{"external": value}, nil
505
+	case types.Dict:
506
+		return map[string]interface{}{"external": true, "name": value["name"]}, nil
507
+	case map[string]interface{}:
508
+		return map[string]interface{}{"external": true, "name": value["name"]}, nil
509
+	default:
510
+		return data, fmt.Errorf("invalid type %T for external", value)
511
+	}
512
+}
513
+
514
+func toYAMLName(name string) string {
515
+	nameParts := fieldNameRegexp.FindAllString(name, -1)
516
+	for i, p := range nameParts {
517
+		nameParts[i] = strings.ToLower(p)
518
+	}
519
+	return strings.Join(nameParts, "_")
520
+}
521
+
522
+func loadListOrStructMap(value interface{}, target reflect.Type) (interface{}, error) {
523
+	if list, ok := value.([]interface{}); ok {
524
+		mapValue := map[interface{}]interface{}{}
525
+		for _, name := range list {
526
+			mapValue[name] = nil
527
+		}
528
+		return mapValue, nil
529
+	}
530
+
531
+	return value, nil
532
+}
533
+
534
+func loadListOfStringsOrNumbers(value interface{}) []string {
535
+	list := value.([]interface{})
536
+	result := make([]string, len(list))
537
+	for i, item := range list {
538
+		result[i] = fmt.Sprint(item)
539
+	}
540
+	return result
541
+}
542
+
543
+func loadStringOrListOfStrings(value interface{}) []string {
544
+	if list, ok := value.([]interface{}); ok {
545
+		result := make([]string, len(list))
546
+		for i, item := range list {
547
+			result[i] = fmt.Sprint(item)
548
+		}
549
+		return result
550
+	}
551
+	return []string{value.(string)}
552
+}
553
+
554
+func loadMappingOrList(mappingOrList interface{}, sep string) map[string]string {
555
+	if mapping, ok := mappingOrList.(types.Dict); ok {
556
+		return toMapStringString(mapping)
557
+	}
558
+	if list, ok := mappingOrList.([]interface{}); ok {
559
+		result := make(map[string]string)
560
+		for _, value := range list {
561
+			parts := strings.SplitN(value.(string), sep, 2)
562
+			if len(parts) == 1 {
563
+				result[parts[0]] = ""
564
+			} else {
565
+				result[parts[0]] = parts[1]
566
+			}
567
+		}
568
+		return result
569
+	}
570
+	panic(fmt.Errorf("expected a map or a slice, got: %#v", mappingOrList))
571
+}
572
+
573
+func loadShellCommand(value interface{}) (interface{}, error) {
574
+	if str, ok := value.(string); ok {
575
+		return shellwords.Parse(str)
576
+	}
577
+	return value, nil
578
+}
579
+
580
+func loadHealthcheck(value interface{}) (interface{}, error) {
581
+	if str, ok := value.(string); ok {
582
+		return append([]string{"CMD-SHELL"}, str), nil
583
+	}
584
+	return value, nil
585
+}
586
+
587
+func loadSize(value interface{}) (int64, error) {
588
+	switch value := value.(type) {
589
+	case int:
590
+		return int64(value), nil
591
+	case string:
592
+		return units.RAMInBytes(value)
593
+	}
594
+	panic(fmt.Errorf("invalid type for size %T", value))
595
+}
596
+
597
+func toMapStringString(value map[string]interface{}) map[string]string {
598
+	output := make(map[string]string)
599
+	for key, value := range value {
600
+		output[key] = toString(value)
601
+	}
602
+	return output
603
+}
604
+
605
+func toString(value interface{}) string {
606
+	if value == nil {
607
+		return ""
608
+	}
609
+	return fmt.Sprint(value)
610
+}
0 611
new file mode 100644
... ...
@@ -0,0 +1,782 @@
0
+package loader
1
+
2
+import (
3
+	"fmt"
4
+	"io/ioutil"
5
+	"os"
6
+	"sort"
7
+	"testing"
8
+	"time"
9
+
10
+	"github.com/docker/docker/cli/compose/types"
11
+	"github.com/stretchr/testify/assert"
12
+)
13
+
14
+func buildConfigDetails(source types.Dict) types.ConfigDetails {
15
+	workingDir, err := os.Getwd()
16
+	if err != nil {
17
+		panic(err)
18
+	}
19
+
20
+	return types.ConfigDetails{
21
+		WorkingDir: workingDir,
22
+		ConfigFiles: []types.ConfigFile{
23
+			{Filename: "filename.yml", Config: source},
24
+		},
25
+		Environment: nil,
26
+	}
27
+}
28
+
29
+var sampleYAML = `
30
+version: "3"
31
+services:
32
+  foo:
33
+    image: busybox
34
+    networks:
35
+      with_me:
36
+  bar:
37
+    image: busybox
38
+    environment:
39
+      - FOO=1
40
+    networks:
41
+      - with_ipam
42
+volumes:
43
+  hello:
44
+    driver: default
45
+    driver_opts:
46
+      beep: boop
47
+networks:
48
+  default:
49
+    driver: bridge
50
+    driver_opts:
51
+      beep: boop
52
+  with_ipam:
53
+    ipam:
54
+      driver: default
55
+      config:
56
+        - subnet: 172.28.0.0/16
57
+`
58
+
59
+var sampleDict = types.Dict{
60
+	"version": "3",
61
+	"services": types.Dict{
62
+		"foo": types.Dict{
63
+			"image":    "busybox",
64
+			"networks": types.Dict{"with_me": nil},
65
+		},
66
+		"bar": types.Dict{
67
+			"image":       "busybox",
68
+			"environment": []interface{}{"FOO=1"},
69
+			"networks":    []interface{}{"with_ipam"},
70
+		},
71
+	},
72
+	"volumes": types.Dict{
73
+		"hello": types.Dict{
74
+			"driver": "default",
75
+			"driver_opts": types.Dict{
76
+				"beep": "boop",
77
+			},
78
+		},
79
+	},
80
+	"networks": types.Dict{
81
+		"default": types.Dict{
82
+			"driver": "bridge",
83
+			"driver_opts": types.Dict{
84
+				"beep": "boop",
85
+			},
86
+		},
87
+		"with_ipam": types.Dict{
88
+			"ipam": types.Dict{
89
+				"driver": "default",
90
+				"config": []interface{}{
91
+					types.Dict{
92
+						"subnet": "172.28.0.0/16",
93
+					},
94
+				},
95
+			},
96
+		},
97
+	},
98
+}
99
+
100
+var sampleConfig = types.Config{
101
+	Services: []types.ServiceConfig{
102
+		{
103
+			Name:        "foo",
104
+			Image:       "busybox",
105
+			Environment: map[string]string{},
106
+			Networks: map[string]*types.ServiceNetworkConfig{
107
+				"with_me": nil,
108
+			},
109
+		},
110
+		{
111
+			Name:        "bar",
112
+			Image:       "busybox",
113
+			Environment: map[string]string{"FOO": "1"},
114
+			Networks: map[string]*types.ServiceNetworkConfig{
115
+				"with_ipam": nil,
116
+			},
117
+		},
118
+	},
119
+	Networks: map[string]types.NetworkConfig{
120
+		"default": {
121
+			Driver: "bridge",
122
+			DriverOpts: map[string]string{
123
+				"beep": "boop",
124
+			},
125
+		},
126
+		"with_ipam": {
127
+			Ipam: types.IPAMConfig{
128
+				Driver: "default",
129
+				Config: []*types.IPAMPool{
130
+					{
131
+						Subnet: "172.28.0.0/16",
132
+					},
133
+				},
134
+			},
135
+		},
136
+	},
137
+	Volumes: map[string]types.VolumeConfig{
138
+		"hello": {
139
+			Driver: "default",
140
+			DriverOpts: map[string]string{
141
+				"beep": "boop",
142
+			},
143
+		},
144
+	},
145
+}
146
+
147
+func TestParseYAML(t *testing.T) {
148
+	dict, err := ParseYAML([]byte(sampleYAML))
149
+	if !assert.NoError(t, err) {
150
+		return
151
+	}
152
+	assert.Equal(t, sampleDict, dict)
153
+}
154
+
155
+func TestLoad(t *testing.T) {
156
+	actual, err := Load(buildConfigDetails(sampleDict))
157
+	if !assert.NoError(t, err) {
158
+		return
159
+	}
160
+	assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
161
+	assert.Equal(t, sampleConfig.Networks, actual.Networks)
162
+	assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
163
+}
164
+
165
+func TestParseAndLoad(t *testing.T) {
166
+	actual, err := loadYAML(sampleYAML)
167
+	if !assert.NoError(t, err) {
168
+		return
169
+	}
170
+	assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
171
+	assert.Equal(t, sampleConfig.Networks, actual.Networks)
172
+	assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
173
+}
174
+
175
+func TestInvalidTopLevelObjectType(t *testing.T) {
176
+	_, err := loadYAML("1")
177
+	assert.Error(t, err)
178
+	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
179
+
180
+	_, err = loadYAML("\"hello\"")
181
+	assert.Error(t, err)
182
+	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
183
+
184
+	_, err = loadYAML("[\"hello\"]")
185
+	assert.Error(t, err)
186
+	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
187
+}
188
+
189
+func TestNonStringKeys(t *testing.T) {
190
+	_, err := loadYAML(`
191
+version: "3"
192
+123:
193
+  foo:
194
+    image: busybox
195
+`)
196
+	assert.Error(t, err)
197
+	assert.Contains(t, err.Error(), "Non-string key at top level: 123")
198
+
199
+	_, err = loadYAML(`
200
+version: "3"
201
+services:
202
+  foo:
203
+    image: busybox
204
+  123:
205
+    image: busybox
206
+`)
207
+	assert.Error(t, err)
208
+	assert.Contains(t, err.Error(), "Non-string key in services: 123")
209
+
210
+	_, err = loadYAML(`
211
+version: "3"
212
+services:
213
+  foo:
214
+    image: busybox
215
+networks:
216
+  default:
217
+    ipam:
218
+      config:
219
+        - 123: oh dear
220
+`)
221
+	assert.Error(t, err)
222
+	assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123")
223
+
224
+	_, err = loadYAML(`
225
+version: "3"
226
+services:
227
+  dict-env:
228
+    image: busybox
229
+    environment:
230
+      1: FOO
231
+`)
232
+	assert.Error(t, err)
233
+	assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1")
234
+}
235
+
236
+func TestSupportedVersion(t *testing.T) {
237
+	_, err := loadYAML(`
238
+version: "3"
239
+services:
240
+  foo:
241
+    image: busybox
242
+`)
243
+	assert.NoError(t, err)
244
+
245
+	_, err = loadYAML(`
246
+version: "3.0"
247
+services:
248
+  foo:
249
+    image: busybox
250
+`)
251
+	assert.NoError(t, err)
252
+}
253
+
254
+func TestUnsupportedVersion(t *testing.T) {
255
+	_, err := loadYAML(`
256
+version: "2"
257
+services:
258
+  foo:
259
+    image: busybox
260
+`)
261
+	assert.Error(t, err)
262
+	assert.Contains(t, err.Error(), "version")
263
+
264
+	_, err = loadYAML(`
265
+version: "2.0"
266
+services:
267
+  foo:
268
+    image: busybox
269
+`)
270
+	assert.Error(t, err)
271
+	assert.Contains(t, err.Error(), "version")
272
+}
273
+
274
+func TestInvalidVersion(t *testing.T) {
275
+	_, err := loadYAML(`
276
+version: 3
277
+services:
278
+  foo:
279
+    image: busybox
280
+`)
281
+	assert.Error(t, err)
282
+	assert.Contains(t, err.Error(), "version must be a string")
283
+}
284
+
285
+func TestV1Unsupported(t *testing.T) {
286
+	_, err := loadYAML(`
287
+foo:
288
+  image: busybox
289
+`)
290
+	assert.Error(t, err)
291
+}
292
+
293
+func TestNonMappingObject(t *testing.T) {
294
+	_, err := loadYAML(`
295
+version: "3"
296
+services:
297
+  - foo:
298
+      image: busybox
299
+`)
300
+	assert.Error(t, err)
301
+	assert.Contains(t, err.Error(), "services must be a mapping")
302
+
303
+	_, err = loadYAML(`
304
+version: "3"
305
+services:
306
+  foo: busybox
307
+`)
308
+	assert.Error(t, err)
309
+	assert.Contains(t, err.Error(), "services.foo must be a mapping")
310
+
311
+	_, err = loadYAML(`
312
+version: "3"
313
+networks:
314
+  - default:
315
+      driver: bridge
316
+`)
317
+	assert.Error(t, err)
318
+	assert.Contains(t, err.Error(), "networks must be a mapping")
319
+
320
+	_, err = loadYAML(`
321
+version: "3"
322
+networks:
323
+  default: bridge
324
+`)
325
+	assert.Error(t, err)
326
+	assert.Contains(t, err.Error(), "networks.default must be a mapping")
327
+
328
+	_, err = loadYAML(`
329
+version: "3"
330
+volumes:
331
+  - data:
332
+      driver: local
333
+`)
334
+	assert.Error(t, err)
335
+	assert.Contains(t, err.Error(), "volumes must be a mapping")
336
+
337
+	_, err = loadYAML(`
338
+version: "3"
339
+volumes:
340
+  data: local
341
+`)
342
+	assert.Error(t, err)
343
+	assert.Contains(t, err.Error(), "volumes.data must be a mapping")
344
+}
345
+
346
+func TestNonStringImage(t *testing.T) {
347
+	_, err := loadYAML(`
348
+version: "3"
349
+services:
350
+  foo:
351
+    image: ["busybox", "latest"]
352
+`)
353
+	assert.Error(t, err)
354
+	assert.Contains(t, err.Error(), "services.foo.image must be a string")
355
+}
356
+
357
+func TestValidEnvironment(t *testing.T) {
358
+	config, err := loadYAML(`
359
+version: "3"
360
+services:
361
+  dict-env:
362
+    image: busybox
363
+    environment:
364
+      FOO: "1"
365
+      BAR: 2
366
+      BAZ: 2.5
367
+      QUUX:
368
+  list-env:
369
+    image: busybox
370
+    environment:
371
+      - FOO=1
372
+      - BAR=2
373
+      - BAZ=2.5
374
+      - QUUX=
375
+`)
376
+	assert.NoError(t, err)
377
+
378
+	expected := map[string]string{
379
+		"FOO":  "1",
380
+		"BAR":  "2",
381
+		"BAZ":  "2.5",
382
+		"QUUX": "",
383
+	}
384
+
385
+	assert.Equal(t, 2, len(config.Services))
386
+
387
+	for _, service := range config.Services {
388
+		assert.Equal(t, expected, service.Environment)
389
+	}
390
+}
391
+
392
+func TestInvalidEnvironmentValue(t *testing.T) {
393
+	_, err := loadYAML(`
394
+version: "3"
395
+services:
396
+  dict-env:
397
+    image: busybox
398
+    environment:
399
+      FOO: ["1"]
400
+`)
401
+	assert.Error(t, err)
402
+	assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null")
403
+}
404
+
405
+func TestInvalidEnvironmentObject(t *testing.T) {
406
+	_, err := loadYAML(`
407
+version: "3"
408
+services:
409
+  dict-env:
410
+    image: busybox
411
+    environment: "FOO=1"
412
+`)
413
+	assert.Error(t, err)
414
+	assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping")
415
+}
416
+
417
+func TestEnvironmentInterpolation(t *testing.T) {
418
+	config, err := loadYAML(`
419
+version: "3"
420
+services:
421
+  test:
422
+    image: busybox
423
+    labels:
424
+      - home1=$HOME
425
+      - home2=${HOME}
426
+      - nonexistent=$NONEXISTENT
427
+      - default=${NONEXISTENT-default}
428
+networks:
429
+  test:
430
+    driver: $HOME
431
+volumes:
432
+  test:
433
+    driver: $HOME
434
+`)
435
+
436
+	assert.NoError(t, err)
437
+
438
+	home := os.Getenv("HOME")
439
+
440
+	expectedLabels := map[string]string{
441
+		"home1":       home,
442
+		"home2":       home,
443
+		"nonexistent": "",
444
+		"default":     "default",
445
+	}
446
+
447
+	assert.Equal(t, expectedLabels, config.Services[0].Labels)
448
+	assert.Equal(t, home, config.Networks["test"].Driver)
449
+	assert.Equal(t, home, config.Volumes["test"].Driver)
450
+}
451
+
452
+func TestUnsupportedProperties(t *testing.T) {
453
+	dict, err := ParseYAML([]byte(`
454
+version: "3"
455
+services:
456
+  web:
457
+    image: web
458
+    build: ./web
459
+    links:
460
+      - bar
461
+  db:
462
+    image: db
463
+    build: ./db
464
+`))
465
+	assert.NoError(t, err)
466
+
467
+	configDetails := buildConfigDetails(dict)
468
+
469
+	_, err = Load(configDetails)
470
+	assert.NoError(t, err)
471
+
472
+	unsupported := GetUnsupportedProperties(configDetails)
473
+	assert.Equal(t, []string{"build", "links"}, unsupported)
474
+}
475
+
476
+func TestDeprecatedProperties(t *testing.T) {
477
+	dict, err := ParseYAML([]byte(`
478
+version: "3"
479
+services:
480
+  web:
481
+    image: web
482
+    container_name: web
483
+  db:
484
+    image: db
485
+    container_name: db
486
+    expose: ["5434"]
487
+`))
488
+	assert.NoError(t, err)
489
+
490
+	configDetails := buildConfigDetails(dict)
491
+
492
+	_, err = Load(configDetails)
493
+	assert.NoError(t, err)
494
+
495
+	deprecated := GetDeprecatedProperties(configDetails)
496
+	assert.Equal(t, 2, len(deprecated))
497
+	assert.Contains(t, deprecated, "container_name")
498
+	assert.Contains(t, deprecated, "expose")
499
+}
500
+
501
+func TestForbiddenProperties(t *testing.T) {
502
+	_, err := loadYAML(`
503
+version: "3"
504
+services:
505
+  foo:
506
+    image: busybox
507
+    volumes:
508
+      - /data
509
+    volume_driver: some-driver
510
+  bar:
511
+    extends:
512
+      service: foo
513
+`)
514
+
515
+	assert.Error(t, err)
516
+	assert.IsType(t, &ForbiddenPropertiesError{}, err)
517
+	fmt.Println(err)
518
+	forbidden := err.(*ForbiddenPropertiesError).Properties
519
+
520
+	assert.Equal(t, 2, len(forbidden))
521
+	assert.Contains(t, forbidden, "volume_driver")
522
+	assert.Contains(t, forbidden, "extends")
523
+}
524
+
525
+func durationPtr(value time.Duration) *time.Duration {
526
+	return &value
527
+}
528
+
529
+func int64Ptr(value int64) *int64 {
530
+	return &value
531
+}
532
+
533
+func uint64Ptr(value uint64) *uint64 {
534
+	return &value
535
+}
536
+
537
+func TestFullExample(t *testing.T) {
538
+	bytes, err := ioutil.ReadFile("full-example.yml")
539
+	assert.NoError(t, err)
540
+
541
+	config, err := loadYAML(string(bytes))
542
+	if !assert.NoError(t, err) {
543
+		return
544
+	}
545
+
546
+	workingDir, err := os.Getwd()
547
+	assert.NoError(t, err)
548
+
549
+	homeDir := os.Getenv("HOME")
550
+	stopGracePeriod := time.Duration(20 * time.Second)
551
+
552
+	expectedServiceConfig := types.ServiceConfig{
553
+		Name: "foo",
554
+
555
+		CapAdd:        []string{"ALL"},
556
+		CapDrop:       []string{"NET_ADMIN", "SYS_ADMIN"},
557
+		CgroupParent:  "m-executor-abcd",
558
+		Command:       []string{"bundle", "exec", "thin", "-p", "3000"},
559
+		ContainerName: "my-web-container",
560
+		DependsOn:     []string{"db", "redis"},
561
+		Deploy: types.DeployConfig{
562
+			Mode:     "replicated",
563
+			Replicas: uint64Ptr(6),
564
+			Labels:   map[string]string{"FOO": "BAR"},
565
+			UpdateConfig: &types.UpdateConfig{
566
+				Parallelism:     uint64Ptr(3),
567
+				Delay:           time.Duration(10 * time.Second),
568
+				FailureAction:   "continue",
569
+				Monitor:         time.Duration(60 * time.Second),
570
+				MaxFailureRatio: 0.3,
571
+			},
572
+			Resources: types.Resources{
573
+				Limits: &types.Resource{
574
+					NanoCPUs:    "0.001",
575
+					MemoryBytes: 50 * 1024 * 1024,
576
+				},
577
+				Reservations: &types.Resource{
578
+					NanoCPUs:    "0.0001",
579
+					MemoryBytes: 20 * 1024 * 1024,
580
+				},
581
+			},
582
+			RestartPolicy: &types.RestartPolicy{
583
+				Condition:   "on_failure",
584
+				Delay:       durationPtr(5 * time.Second),
585
+				MaxAttempts: uint64Ptr(3),
586
+				Window:      durationPtr(2 * time.Minute),
587
+			},
588
+			Placement: types.Placement{
589
+				Constraints: []string{"node=foo"},
590
+			},
591
+		},
592
+		Devices:    []string{"/dev/ttyUSB0:/dev/ttyUSB0"},
593
+		DNS:        []string{"8.8.8.8", "9.9.9.9"},
594
+		DNSSearch:  []string{"dc1.example.com", "dc2.example.com"},
595
+		DomainName: "foo.com",
596
+		Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"},
597
+		Environment: map[string]string{
598
+			"RACK_ENV":       "development",
599
+			"SHOW":           "true",
600
+			"SESSION_SECRET": "",
601
+			"FOO":            "1",
602
+			"BAR":            "2",
603
+			"BAZ":            "3",
604
+		},
605
+		Expose: []string{"3000", "8000"},
606
+		ExternalLinks: []string{
607
+			"redis_1",
608
+			"project_db_1:mysql",
609
+			"project_db_1:postgresql",
610
+		},
611
+		ExtraHosts: map[string]string{
612
+			"otherhost": "50.31.209.229",
613
+			"somehost":  "162.242.195.82",
614
+		},
615
+		HealthCheck: &types.HealthCheckConfig{
616
+			Test: []string{
617
+				"CMD-SHELL",
618
+				"echo \"hello world\"",
619
+			},
620
+			Interval: "10s",
621
+			Timeout:  "1s",
622
+			Retries:  uint64Ptr(5),
623
+		},
624
+		Hostname: "foo",
625
+		Image:    "redis",
626
+		Ipc:      "host",
627
+		Labels: map[string]string{
628
+			"com.example.description": "Accounting webapp",
629
+			"com.example.number":      "42",
630
+			"com.example.empty-label": "",
631
+		},
632
+		Links: []string{
633
+			"db",
634
+			"db:database",
635
+			"redis",
636
+		},
637
+		Logging: &types.LoggingConfig{
638
+			Driver: "syslog",
639
+			Options: map[string]string{
640
+				"syslog-address": "tcp://192.168.0.42:123",
641
+			},
642
+		},
643
+		MacAddress:  "02:42:ac:11:65:43",
644
+		NetworkMode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b",
645
+		Networks: map[string]*types.ServiceNetworkConfig{
646
+			"some-network": {
647
+				Aliases:     []string{"alias1", "alias3"},
648
+				Ipv4Address: "",
649
+				Ipv6Address: "",
650
+			},
651
+			"other-network": {
652
+				Ipv4Address: "172.16.238.10",
653
+				Ipv6Address: "2001:3984:3989::10",
654
+			},
655
+			"other-other-network": nil,
656
+		},
657
+		Pid: "host",
658
+		Ports: []string{
659
+			"3000",
660
+			"3000-3005",
661
+			"8000:8000",
662
+			"9090-9091:8080-8081",
663
+			"49100:22",
664
+			"127.0.0.1:8001:8001",
665
+			"127.0.0.1:5000-5010:5000-5010",
666
+		},
667
+		Privileged: true,
668
+		ReadOnly:   true,
669
+		Restart:    "always",
670
+		SecurityOpt: []string{
671
+			"label=level:s0:c100,c200",
672
+			"label=type:svirt_apache_t",
673
+		},
674
+		StdinOpen:       true,
675
+		StopSignal:      "SIGUSR1",
676
+		StopGracePeriod: &stopGracePeriod,
677
+		Tmpfs:           []string{"/run", "/tmp"},
678
+		Tty:             true,
679
+		Ulimits: map[string]*types.UlimitsConfig{
680
+			"nproc": {
681
+				Single: 65535,
682
+			},
683
+			"nofile": {
684
+				Soft: 20000,
685
+				Hard: 40000,
686
+			},
687
+		},
688
+		User: "someone",
689
+		Volumes: []string{
690
+			"/var/lib/mysql",
691
+			"/opt/data:/var/lib/mysql",
692
+			fmt.Sprintf("%s:/code", workingDir),
693
+			fmt.Sprintf("%s/static:/var/www/html", workingDir),
694
+			fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir),
695
+			"datavolume:/var/lib/mysql",
696
+		},
697
+		WorkingDir: "/code",
698
+	}
699
+
700
+	assert.Equal(t, []types.ServiceConfig{expectedServiceConfig}, config.Services)
701
+
702
+	expectedNetworkConfig := map[string]types.NetworkConfig{
703
+		"some-network": {},
704
+
705
+		"other-network": {
706
+			Driver: "overlay",
707
+			DriverOpts: map[string]string{
708
+				"foo": "bar",
709
+				"baz": "1",
710
+			},
711
+			Ipam: types.IPAMConfig{
712
+				Driver: "overlay",
713
+				Config: []*types.IPAMPool{
714
+					{Subnet: "172.16.238.0/24"},
715
+					{Subnet: "2001:3984:3989::/64"},
716
+				},
717
+			},
718
+		},
719
+
720
+		"external-network": {
721
+			External: types.External{
722
+				Name:     "external-network",
723
+				External: true,
724
+			},
725
+		},
726
+
727
+		"other-external-network": {
728
+			External: types.External{
729
+				Name:     "my-cool-network",
730
+				External: true,
731
+			},
732
+		},
733
+	}
734
+
735
+	assert.Equal(t, expectedNetworkConfig, config.Networks)
736
+
737
+	expectedVolumeConfig := map[string]types.VolumeConfig{
738
+		"some-volume": {},
739
+		"other-volume": {
740
+			Driver: "flocker",
741
+			DriverOpts: map[string]string{
742
+				"foo": "bar",
743
+				"baz": "1",
744
+			},
745
+		},
746
+		"external-volume": {
747
+			External: types.External{
748
+				Name:     "external-volume",
749
+				External: true,
750
+			},
751
+		},
752
+		"other-external-volume": {
753
+			External: types.External{
754
+				Name:     "my-cool-volume",
755
+				External: true,
756
+			},
757
+		},
758
+	}
759
+
760
+	assert.Equal(t, expectedVolumeConfig, config.Volumes)
761
+}
762
+
763
+func loadYAML(yaml string) (*types.Config, error) {
764
+	dict, err := ParseYAML([]byte(yaml))
765
+	if err != nil {
766
+		return nil, err
767
+	}
768
+
769
+	return Load(buildConfigDetails(dict))
770
+}
771
+
772
+func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
773
+	sort.Sort(servicesByName(services))
774
+	return services
775
+}
776
+
777
+type servicesByName []types.ServiceConfig
778
+
779
+func (sbn servicesByName) Len() int           { return len(sbn) }
780
+func (sbn servicesByName) Swap(i, j int)      { sbn[i], sbn[j] = sbn[j], sbn[i] }
781
+func (sbn servicesByName) Less(i, j int) bool { return sbn[i].Name < sbn[j].Name }
0 782
new file mode 100644
... ...
@@ -0,0 +1,237 @@
0
+// Code generated by go-bindata.
1
+// sources:
2
+// data/config_schema_v3.0.json
3
+// DO NOT EDIT!
4
+
5
+package schema
6
+
7
+import (
8
+	"bytes"
9
+	"compress/gzip"
10
+	"fmt"
11
+	"io"
12
+	"io/ioutil"
13
+	"os"
14
+	"path/filepath"
15
+	"strings"
16
+	"time"
17
+)
18
+
19
+func bindataRead(data []byte, name string) ([]byte, error) {
20
+	gz, err := gzip.NewReader(bytes.NewBuffer(data))
21
+	if err != nil {
22
+		return nil, fmt.Errorf("Read %q: %v", name, err)
23
+	}
24
+
25
+	var buf bytes.Buffer
26
+	_, err = io.Copy(&buf, gz)
27
+	clErr := gz.Close()
28
+
29
+	if err != nil {
30
+		return nil, fmt.Errorf("Read %q: %v", name, err)
31
+	}
32
+	if clErr != nil {
33
+		return nil, err
34
+	}
35
+
36
+	return buf.Bytes(), nil
37
+}
38
+
39
+type asset struct {
40
+	bytes []byte
41
+	info  os.FileInfo
42
+}
43
+
44
+type bindataFileInfo struct {
45
+	name    string
46
+	size    int64
47
+	mode    os.FileMode
48
+	modTime time.Time
49
+}
50
+
51
+func (fi bindataFileInfo) Name() string {
52
+	return fi.name
53
+}
54
+func (fi bindataFileInfo) Size() int64 {
55
+	return fi.size
56
+}
57
+func (fi bindataFileInfo) Mode() os.FileMode {
58
+	return fi.mode
59
+}
60
+func (fi bindataFileInfo) ModTime() time.Time {
61
+	return fi.modTime
62
+}
63
+func (fi bindataFileInfo) IsDir() bool {
64
+	return false
65
+}
66
+func (fi bindataFileInfo) Sys() interface{} {
67
+	return nil
68
+}
69
+
70
+var _dataConfig_schema_v30Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\x4d\x8f\xdb\x36\x13\xbe\xfb\x57\x08\x4a\x6e\xf1\xee\x06\x78\x83\x17\x68\x6e\x3d\xf6\xd4\x9e\xbb\x50\x04\x5a\x1a\xdb\xcc\x52\x24\x33\xa4\x9c\x75\x02\xff\xf7\x82\xfa\xb2\x48\x93\xa2\x6c\x2b\x4d\x0e\xbd\x2c\xd6\xe2\xcc\x70\xbe\xf8\xcc\x70\xa4\xef\xab\x24\x49\xdf\xaa\x62\x0f\x15\x49\x3f\x26\xe9\x5e\x6b\xf9\xf1\xe9\xe9\xb3\x12\xfc\xa1\x7d\xfa\x28\x70\xf7\x54\x22\xd9\xea\x87\xf7\x1f\x9e\xda\x67\x6f\xd2\xb5\xe1\xa3\xa5\x61\x29\x04\xdf\xd2\x5d\xde\xae\xe4\x87\xff\x3d\xbe\x7f\x34\xec\x2d\x89\x3e\x4a\x30\x44\x62\xf3\x19\x0a\xdd\x3e\x43\xf8\x52\x53\x04\xc3\xfc\x9c\x1e\x00\x15\x15\x3c\xcd\xd6\x2b\xb3\x26\x51\x48\x40\x4d\x41\xa5\x1f\x13\xa3\x5c\x92\x0c\x24\xfd\x83\x91\x58\xa5\x91\xf2\x5d\xda\x3c\x3e\x35\x12\x92\x24\x55\x80\x07\x5a\x8c\x24\x0c\xaa\xbe\x79\x3a\xcb\x7f\x1a\xc8\xd6\xae\xd4\x91\xb2\xcd\x73\x49\xb4\x06\xe4\x7f\x5d\xea\xd6\x2c\x7f\x7a\x26\x0f\xdf\x7e\x7f\xf8\xfb\xfd\xc3\x6f\x8f\xf9\x43\xf6\xee\xad\xb5\x6c\xfc\x8b\xb0\x6d\xb7\x2f\x61\x4b\x39\xd5\x54\xf0\x61\xff\x74\xa0\x3c\x75\xff\x9d\x86\x8d\x49\x59\x36\xc4\x84\x59\x7b\x6f\x09\x53\x60\xdb\xcc\x41\x7f\x15\xf8\x12\xb3\x79\x20\xfb\x49\x36\x77\xfb\x7b\x6c\xb6\xcd\x39\x08\x56\x57\xd1\x08\xf6\x54\x3f\xc9\x98\x76\xfb\xfb\xe2\xb7\xea\x8d\x9e\xa4\x6d\x29\x46\x7b\x37\x0a\x5a\xd9\xee\x73\x95\x2f\xdb\xc2\xbe\x1a\x9c\x15\xf0\x52\x09\x92\x89\xa3\x79\x16\xf0\x47\x4b\x50\x01\xd7\xe9\xe0\x82\x24\x49\x37\x35\x65\xa5\xeb\x51\xc1\xe1\x4f\x23\xe2\x79\xf4\x30\x49\xbe\xbb\x07\x7b\x24\xa7\x59\xb7\x7e\x85\x03\x3e\xac\x07\x6c\x19\xd6\x0b\xc1\x35\xbc\xea\xc6\xa8\xe9\xad\x5b\x17\x88\xe2\x05\x70\x4b\x19\xcc\xe5\x20\xb8\x53\x13\x2e\x63\x54\xe9\x5c\x60\x5e\xd2\x42\xa7\x27\x87\xfd\x42\x5e\x3c\x9f\x06\xd6\xd1\xaf\x6c\xe5\x11\x98\x16\x44\xe6\xa4\x2c\x2d\x3b\x08\x22\x39\xa6\xeb\x24\xa5\x1a\x2a\xe5\x37\x31\x49\x6b\x4e\xbf\xd4\xf0\x47\x47\xa2\xb1\x06\x57\x6e\x89\x42\x2e\x2f\x78\x87\xa2\x96\xb9\x24\x68\x12\x6c\xda\xfd\x69\x21\xaa\x8a\xf0\xa5\xb2\xee\x1a\x3b\x66\x78\x5e\x70\x4d\x28\x07\xcc\x39\xa9\x62\x89\x64\x4e\x1d\xf0\x52\xe5\x6d\xfd\x9b\x4c\xa3\x6d\xde\xf2\x2b\x47\xc0\x50\x0c\x17\x8d\x47\xc9\xa7\x12\xbb\x15\x63\x52\xdb\xe8\x96\x3a\x8c\xb9\x02\x82\xc5\xfe\x46\x7e\x51\x11\xca\xe7\xf8\x0e\xb8\xc6\xa3\x14\xb4\xcd\x97\x5f\x2e\x11\x80\x1f\xf2\x01\x4b\xae\x76\x03\xf0\x03\x45\xc1\xab\xfe\x34\xcc\x01\x98\x01\xe4\x0d\xff\xab\x14\x0a\x5c\xc7\x38\x06\x8e\x97\x06\x53\x2d\x9f\xf4\x1c\xcf\xbd\xe1\xeb\x24\xe5\x75\xb5\x01\x34\x2d\x9d\x45\xb9\x15\x58\x11\xa3\x6c\xbf\xf7\x68\xd9\xf2\xb4\x27\xf3\xc6\x0e\x1c\xdb\x60\xca\x3a\x61\x39\xa3\xfc\x65\xf9\x14\x87\x57\x8d\x24\xdf\x0b\xa5\xe7\x63\xf8\x88\x7d\x0f\x84\xe9\x7d\xb1\x87\xe2\x65\x82\x7d\x4c\x65\x71\x0b\xa5\xe7\x24\x39\xad\xc8\x2e\x4e\x24\x8b\x18\x09\x23\x1b\x60\x37\xd9\xb9\xa8\xf3\x47\x62\xc5\x6e\x67\x48\x43\x19\x77\xd1\xb9\x74\xcb\xb1\x9a\x5f\x22\x3d\x00\xce\x2d\xe0\x42\x9e\x1b\x2e\x77\x31\xde\x80\x24\xf1\xee\xd3\x22\xfd\xf4\xd8\x36\x9f\x13\xa7\xaa\xf9\x8f\xb1\x34\x73\xdb\x85\xc4\xa9\xfb\xbe\x27\x8e\x85\xf3\x1a\x0a\x2b\x2a\x15\x29\x4c\xdf\x80\xa0\x02\x71\x3d\x93\x76\xcd\x7e\x5e\x89\x32\x94\xa0\x17\xc4\xae\x6f\x82\x48\x7d\x75\x21\x4c\x6e\xea\x1f\x67\x85\x2e\x7a\x81\x88\x58\x13\x52\x6f\xae\x9a\x67\x75\xe3\x29\xd6\xd0\x11\x46\x89\x82\xf8\x61\x0f\x3a\xd2\x92\x46\xe5\xe1\xc3\xcc\x9c\xf0\xf1\xfe\x7f\x92\x37\xc0\x1a\x94\x39\xbf\x47\x8e\x88\x3a\xab\xd2\x1c\x37\x9f\x22\x59\xe4\xb4\xfd\xe0\x16\x5e\xd2\x32\x8c\x15\x0d\x42\x8c\x0f\x98\x14\xa8\x2f\x4e\xd7\xbf\x53\xee\xdb\xad\xef\xae\xf6\x12\xe9\x81\x32\xd8\x81\x7d\x6b\xd9\x08\xc1\x80\x70\x0b\x7a\x10\x48\x99\x0b\xce\x8e\x33\x28\x95\x26\x18\xbd\x50\x28\x28\x6a\xa4\xfa\x98\x0b\xa9\x17\xef\x33\xd4\xbe\xca\x15\xfd\x06\x76\x34\xcf\x78\xdf\x09\xca\x2c\x1e\x5d\x52\x9e\x0b\x09\x3c\x6a\xa2\xd2\x42\xe6\x8a\xee\x38\x61\x51\x33\x0d\xe9\x0e\x49\x01\xb9\x04\xa4\xa2\xf4\x31\xac\xc7\xb1\x2d\x6b\x24\x26\x9f\x2d\x31\xba\x92\xdb\x1b\x6f\x07\x5a\xc7\x63\x56\x33\x5a\xd1\x70\x32\x7b\x50\x72\x06\x90\xb7\x20\xee\xc7\xee\x09\xdc\x3e\x6b\x4a\xb9\x86\x1d\xa0\x0f\xee\x26\x5a\x87\xe9\xce\x61\x46\xcb\xb0\x27\x68\x47\x69\x42\x8f\x86\x41\x89\xad\xf6\x33\xf8\x1a\x0a\xaf\x5e\xd6\x04\xb7\x91\xb7\xee\x14\xc9\xbc\xf4\x57\x61\xb2\xab\x46\x16\x84\xc5\x93\x17\x16\x6b\x15\xed\xee\xc6\xf3\xc5\x45\x4f\xb2\x69\x61\x4c\x66\x97\xd4\xaf\xc2\xca\x51\xf7\x8a\x09\xaf\x73\x9b\xe8\x05\xf8\x66\x7d\x63\x52\x77\xde\xf7\x3c\x24\x5c\x5f\x25\xce\x53\xd2\xc0\xe0\xcf\xe4\x07\x1e\x2c\xf0\xf0\xf9\x54\xd3\x0a\x44\xad\x23\x54\x08\x1a\xa9\xe3\xf9\x0e\xe9\x2c\x61\xa0\x7e\xcd\x4b\x7b\x49\x15\xd9\x38\xf3\xbf\x01\xa3\x6e\x0a\x6f\x72\x1e\xae\xf6\x97\xf9\xa9\xe0\x8e\x28\x17\x88\xed\x44\x6f\x3e\x0a\x99\x64\xb4\x20\x2a\x86\x32\x77\x5c\x21\x6b\x59\x12\x0d\x79\xfb\x2a\xe9\x2a\x5c\x9f\x00\x74\x49\x90\x30\x06\x8c\xaa\x6a\x0e\x40\xa6\x25\x30\x72\xbc\xa9\xe0\x35\xec\x5b\x42\x59\x8d\x90\x93\x42\x77\x6f\xab\x22\x99\x99\x56\x82\x53\x2d\xbc\x48\x31\x6f\xcb\x8a\xbc\xe6\xfd\xb6\x0d\x89\xf7\x58\x05\x1b\xaf\xb9\xb7\xbf\x51\x26\x28\x51\x63\x71\xe1\xec\x9b\x43\x74\x2e\xe4\x81\x8c\xe9\x77\xbc\x30\x1d\x41\x19\x50\x1a\x2e\xe7\x51\xfe\x68\xdd\xe8\x3a\xc1\x5c\x0a\x46\x8b\xe3\x52\x16\x16\x82\xb7\x4e\x9e\x93\x10\x77\x66\xa0\x49\x07\xd3\xe7\x54\x52\x47\x0f\x6b\xc3\xf0\x95\xf2\x52\x7c\xbd\x62\xc3\xe5\x52\x49\x32\x52\x80\x83\x77\xf7\x3a\x5a\x69\x24\x94\xeb\xab\xcb\xfa\xbd\x66\xdd\x51\xd5\x87\xfc\x8c\xa0\xfe\x40\x17\x7f\xd7\x19\x40\xfa\x42\xd6\xd1\x89\x4d\x05\x95\x40\x6f\x02\x2e\xf0\x6e\x3a\x66\x62\x4f\xb6\x40\x55\x9b\x35\xe2\xeb\xa8\xcc\x8d\x6e\xf1\xab\x44\x7c\x8c\x97\xc5\x01\x89\x4a\x52\x2d\x75\x3a\x66\x0f\x3d\x53\x6f\x0d\x4e\xa6\x87\x05\x49\x78\x60\x10\xd3\x3a\xae\x7b\x47\xa1\xea\x0d\x07\xff\x3d\xfd\xf2\x0a\xe1\x7b\x13\x3b\xff\x0e\x72\x0a\xdf\x38\xee\x03\xbd\xfe\x7d\x45\x20\xaa\xcf\x43\x27\xb9\x1e\x7c\x95\xcd\x0e\x71\xf0\x65\xc1\x72\xfa\x5f\xd9\xe0\xdd\x81\x19\xdd\xb7\x15\x11\xc8\xe8\xa8\xfe\x43\x8c\x5f\x26\xbf\x26\x8a\xe2\x8d\xb7\x83\x2b\x92\xc6\x19\x2b\x8d\x92\xe7\xf2\xea\x38\x15\xe7\xd9\x43\xf1\x8e\x23\xb3\xd5\x70\xc9\x3c\xdf\xad\xd9\x10\x3a\x35\x71\xe8\x49\x02\x43\x52\x67\xd3\xce\x79\xd3\x96\x2f\x98\xb6\x8f\xef\x26\x0a\xc5\xd4\xcb\xab\x1f\x84\xb0\x0b\x4c\x73\xfc\x31\x75\xba\xcb\xde\xbb\x97\x1f\x5f\x05\x90\x6a\xc4\x7f\xf1\x29\x96\xb1\x93\x1f\x2f\x46\x1b\xdf\xed\x31\x5b\xfb\x19\x55\x66\xf9\xc7\x21\x69\x5f\x05\x8f\x70\x22\x1b\x37\xdc\xa1\x30\x7a\x3f\xd0\x72\x87\x7c\xfd\x87\x52\xd9\xf4\x61\x5f\xf5\x7f\x4f\xab\xd3\xea\x9f\x00\x00\x00\xff\xff\xd1\xeb\xc9\xb9\x5c\x2a\x00\x00")
71
+
72
+func dataConfig_schema_v30JsonBytes() ([]byte, error) {
73
+	return bindataRead(
74
+		_dataConfig_schema_v30Json,
75
+		"data/config_schema_v3.0.json",
76
+	)
77
+}
78
+
79
+func dataConfig_schema_v30Json() (*asset, error) {
80
+	bytes, err := dataConfig_schema_v30JsonBytes()
81
+	if err != nil {
82
+		return nil, err
83
+	}
84
+
85
+	info := bindataFileInfo{name: "data/config_schema_v3.0.json", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)}
86
+	a := &asset{bytes: bytes, info: info}
87
+	return a, nil
88
+}
89
+
90
+// Asset loads and returns the asset for the given name.
91
+// It returns an error if the asset could not be found or
92
+// could not be loaded.
93
+func Asset(name string) ([]byte, error) {
94
+	cannonicalName := strings.Replace(name, "\\", "/", -1)
95
+	if f, ok := _bindata[cannonicalName]; ok {
96
+		a, err := f()
97
+		if err != nil {
98
+			return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
99
+		}
100
+		return a.bytes, nil
101
+	}
102
+	return nil, fmt.Errorf("Asset %s not found", name)
103
+}
104
+
105
+// MustAsset is like Asset but panics when Asset would return an error.
106
+// It simplifies safe initialization of global variables.
107
+func MustAsset(name string) []byte {
108
+	a, err := Asset(name)
109
+	if err != nil {
110
+		panic("asset: Asset(" + name + "): " + err.Error())
111
+	}
112
+
113
+	return a
114
+}
115
+
116
+// AssetInfo loads and returns the asset info for the given name.
117
+// It returns an error if the asset could not be found or
118
+// could not be loaded.
119
+func AssetInfo(name string) (os.FileInfo, error) {
120
+	cannonicalName := strings.Replace(name, "\\", "/", -1)
121
+	if f, ok := _bindata[cannonicalName]; ok {
122
+		a, err := f()
123
+		if err != nil {
124
+			return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
125
+		}
126
+		return a.info, nil
127
+	}
128
+	return nil, fmt.Errorf("AssetInfo %s not found", name)
129
+}
130
+
131
+// AssetNames returns the names of the assets.
132
+func AssetNames() []string {
133
+	names := make([]string, 0, len(_bindata))
134
+	for name := range _bindata {
135
+		names = append(names, name)
136
+	}
137
+	return names
138
+}
139
+
140
+// _bindata is a table, holding each asset generator, mapped to its name.
141
+var _bindata = map[string]func() (*asset, error){
142
+	"data/config_schema_v3.0.json": dataConfig_schema_v30Json,
143
+}
144
+
145
+// AssetDir returns the file names below a certain
146
+// directory embedded in the file by go-bindata.
147
+// For example if you run go-bindata on data/... and data contains the
148
+// following hierarchy:
149
+//     data/
150
+//       foo.txt
151
+//       img/
152
+//         a.png
153
+//         b.png
154
+// then AssetDir("data") would return []string{"foo.txt", "img"}
155
+// AssetDir("data/img") would return []string{"a.png", "b.png"}
156
+// AssetDir("foo.txt") and AssetDir("notexist") would return an error
157
+// AssetDir("") will return []string{"data"}.
158
+func AssetDir(name string) ([]string, error) {
159
+	node := _bintree
160
+	if len(name) != 0 {
161
+		cannonicalName := strings.Replace(name, "\\", "/", -1)
162
+		pathList := strings.Split(cannonicalName, "/")
163
+		for _, p := range pathList {
164
+			node = node.Children[p]
165
+			if node == nil {
166
+				return nil, fmt.Errorf("Asset %s not found", name)
167
+			}
168
+		}
169
+	}
170
+	if node.Func != nil {
171
+		return nil, fmt.Errorf("Asset %s not found", name)
172
+	}
173
+	rv := make([]string, 0, len(node.Children))
174
+	for childName := range node.Children {
175
+		rv = append(rv, childName)
176
+	}
177
+	return rv, nil
178
+}
179
+
180
+type bintree struct {
181
+	Func     func() (*asset, error)
182
+	Children map[string]*bintree
183
+}
184
+var _bintree = &bintree{nil, map[string]*bintree{
185
+	"data": &bintree{nil, map[string]*bintree{
186
+		"config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}},
187
+	}},
188
+}}
189
+
190
+// RestoreAsset restores an asset under the given directory
191
+func RestoreAsset(dir, name string) error {
192
+	data, err := Asset(name)
193
+	if err != nil {
194
+		return err
195
+	}
196
+	info, err := AssetInfo(name)
197
+	if err != nil {
198
+		return err
199
+	}
200
+	err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
201
+	if err != nil {
202
+		return err
203
+	}
204
+	err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
205
+	if err != nil {
206
+		return err
207
+	}
208
+	err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
209
+	if err != nil {
210
+		return err
211
+	}
212
+	return nil
213
+}
214
+
215
+// RestoreAssets restores an asset under the given directory recursively
216
+func RestoreAssets(dir, name string) error {
217
+	children, err := AssetDir(name)
218
+	// File
219
+	if err != nil {
220
+		return RestoreAsset(dir, name)
221
+	}
222
+	// Dir
223
+	for _, child := range children {
224
+		err = RestoreAssets(dir, filepath.Join(name, child))
225
+		if err != nil {
226
+			return err
227
+		}
228
+	}
229
+	return nil
230
+}
231
+
232
+func _filePath(dir, name string) string {
233
+	cannonicalName := strings.Replace(name, "\\", "/", -1)
234
+	return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
235
+}
236
+
0 237
new file mode 100644
... ...
@@ -0,0 +1,379 @@
0
+{
1
+  "$schema": "http://json-schema.org/draft-04/schema#",
2
+  "id": "config_schema_v3.0.json",
3
+  "type": "object",
4
+  "required": ["version"],
5
+
6
+  "properties": {
7
+    "version": {
8
+      "type": "string"
9
+    },
10
+
11
+    "services": {
12
+      "id": "#/properties/services",
13
+      "type": "object",
14
+      "patternProperties": {
15
+        "^[a-zA-Z0-9._-]+$": {
16
+          "$ref": "#/definitions/service"
17
+        }
18
+      },
19
+      "additionalProperties": false
20
+    },
21
+
22
+    "networks": {
23
+      "id": "#/properties/networks",
24
+      "type": "object",
25
+      "patternProperties": {
26
+        "^[a-zA-Z0-9._-]+$": {
27
+          "$ref": "#/definitions/network"
28
+        }
29
+      }
30
+    },
31
+
32
+    "volumes": {
33
+      "id": "#/properties/volumes",
34
+      "type": "object",
35
+      "patternProperties": {
36
+        "^[a-zA-Z0-9._-]+$": {
37
+          "$ref": "#/definitions/volume"
38
+        }
39
+      },
40
+      "additionalProperties": false
41
+    }
42
+  },
43
+
44
+  "additionalProperties": false,
45
+
46
+  "definitions": {
47
+
48
+    "service": {
49
+      "id": "#/definitions/service",
50
+      "type": "object",
51
+
52
+      "properties": {
53
+        "deploy": {"$ref": "#/definitions/deployment"},
54
+        "build": {
55
+          "oneOf": [
56
+            {"type": "string"},
57
+            {
58
+              "type": "object",
59
+              "properties": {
60
+                "context": {"type": "string"},
61
+                "dockerfile": {"type": "string"},
62
+                "args": {"$ref": "#/definitions/list_or_dict"}
63
+              },
64
+              "additionalProperties": false
65
+            }
66
+          ]
67
+        },
68
+        "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
69
+        "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
70
+        "cgroup_parent": {"type": "string"},
71
+        "command": {
72
+          "oneOf": [
73
+            {"type": "string"},
74
+            {"type": "array", "items": {"type": "string"}}
75
+          ]
76
+        },
77
+        "container_name": {"type": "string"},
78
+        "depends_on": {"$ref": "#/definitions/list_of_strings"},
79
+        "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
80
+        "dns": {"$ref": "#/definitions/string_or_list"},
81
+        "dns_search": {"$ref": "#/definitions/string_or_list"},
82
+        "domainname": {"type": "string"},
83
+        "entrypoint": {
84
+          "oneOf": [
85
+            {"type": "string"},
86
+            {"type": "array", "items": {"type": "string"}}
87
+          ]
88
+        },
89
+        "env_file": {"$ref": "#/definitions/string_or_list"},
90
+        "environment": {"$ref": "#/definitions/list_or_dict"},
91
+
92
+        "expose": {
93
+          "type": "array",
94
+          "items": {
95
+            "type": ["string", "number"],
96
+            "format": "expose"
97
+          },
98
+          "uniqueItems": true
99
+        },
100
+
101
+        "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
102
+        "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
103
+        "healthcheck": {"$ref": "#/definitions/healthcheck"},
104
+        "hostname": {"type": "string"},
105
+        "image": {"type": "string"},
106
+        "ipc": {"type": "string"},
107
+        "labels": {"$ref": "#/definitions/list_or_dict"},
108
+        "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
109
+
110
+        "logging": {
111
+            "type": "object",
112
+
113
+            "properties": {
114
+                "driver": {"type": "string"},
115
+                "options": {
116
+                  "type": "object",
117
+                  "patternProperties": {
118
+                    "^.+$": {"type": ["string", "number", "null"]}
119
+                  }
120
+                }
121
+            },
122
+            "additionalProperties": false
123
+        },
124
+
125
+        "mac_address": {"type": "string"},
126
+        "network_mode": {"type": "string"},
127
+
128
+        "networks": {
129
+          "oneOf": [
130
+            {"$ref": "#/definitions/list_of_strings"},
131
+            {
132
+              "type": "object",
133
+              "patternProperties": {
134
+                "^[a-zA-Z0-9._-]+$": {
135
+                  "oneOf": [
136
+                    {
137
+                      "type": "object",
138
+                      "properties": {
139
+                        "aliases": {"$ref": "#/definitions/list_of_strings"},
140
+                        "ipv4_address": {"type": "string"},
141
+                        "ipv6_address": {"type": "string"}
142
+                      },
143
+                      "additionalProperties": false
144
+                    },
145
+                    {"type": "null"}
146
+                  ]
147
+                }
148
+              },
149
+              "additionalProperties": false
150
+            }
151
+          ]
152
+        },
153
+        "pid": {"type": ["string", "null"]},
154
+
155
+        "ports": {
156
+          "type": "array",
157
+          "items": {
158
+            "type": ["string", "number"],
159
+            "format": "ports"
160
+          },
161
+          "uniqueItems": true
162
+        },
163
+
164
+        "privileged": {"type": "boolean"},
165
+        "read_only": {"type": "boolean"},
166
+        "restart": {"type": "string"},
167
+        "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
168
+        "shm_size": {"type": ["number", "string"]},
169
+        "stdin_open": {"type": "boolean"},
170
+        "stop_signal": {"type": "string"},
171
+        "stop_grace_period": {"type": "string", "format": "duration"},
172
+        "tmpfs": {"$ref": "#/definitions/string_or_list"},
173
+        "tty": {"type": "boolean"},
174
+        "ulimits": {
175
+          "type": "object",
176
+          "patternProperties": {
177
+            "^[a-z]+$": {
178
+              "oneOf": [
179
+                {"type": "integer"},
180
+                {
181
+                  "type":"object",
182
+                  "properties": {
183
+                    "hard": {"type": "integer"},
184
+                    "soft": {"type": "integer"}
185
+                  },
186
+                  "required": ["soft", "hard"],
187
+                  "additionalProperties": false
188
+                }
189
+              ]
190
+            }
191
+          }
192
+        },
193
+        "user": {"type": "string"},
194
+        "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
195
+        "working_dir": {"type": "string"}
196
+      },
197
+      "additionalProperties": false
198
+    },
199
+
200
+    "healthcheck": {
201
+      "id": "#/definitions/healthcheck",
202
+      "type": ["object", "null"],
203
+      "properties": {
204
+        "interval": {"type":"string"},
205
+        "timeout": {"type":"string"},
206
+        "retries": {"type": "number"},
207
+        "test": {
208
+          "oneOf": [
209
+            {"type": "string"},
210
+            {"type": "array", "items": {"type": "string"}}
211
+          ]
212
+        },
213
+        "disable": {"type": "boolean"}
214
+      },
215
+      "additionalProperties": false
216
+    },
217
+    "deployment": {
218
+      "id": "#/definitions/deployment",
219
+      "type": ["object", "null"],
220
+      "properties": {
221
+        "mode": {"type": "string"},
222
+        "replicas": {"type": "integer"},
223
+        "labels": {"$ref": "#/definitions/list_or_dict"},
224
+        "update_config": {
225
+          "type": "object",
226
+          "properties": {
227
+            "parallelism": {"type": "integer"},
228
+            "delay": {"type": "string", "format": "duration"},
229
+            "failure_action": {"type": "string"},
230
+            "monitor": {"type": "string", "format": "duration"},
231
+            "max_failure_ratio": {"type": "number"}
232
+          },
233
+          "additionalProperties": false
234
+        },
235
+        "resources": {
236
+          "type": "object",
237
+          "properties": {
238
+            "limits": {"$ref": "#/definitions/resource"},
239
+            "reservations": {"$ref": "#/definitions/resource"}
240
+          }
241
+        },
242
+        "restart_policy": {
243
+          "type": "object",
244
+          "properties": {
245
+            "condition": {"type": "string"},
246
+            "delay": {"type": "string", "format": "duration"},
247
+            "max_attempts": {"type": "integer"},
248
+            "window": {"type": "string", "format": "duration"}
249
+          },
250
+          "additionalProperties": false
251
+        },
252
+        "placement": {
253
+          "type": "object",
254
+          "properties": {
255
+            "constraints": {"type": "array", "items": {"type": "string"}}
256
+          },
257
+          "additionalProperties": false
258
+        }
259
+      },
260
+      "additionalProperties": false
261
+    },
262
+
263
+    "resource": {
264
+      "id": "#/definitions/resource",
265
+      "type": "object",
266
+      "properties": {
267
+        "cpus": {"type": "string"},
268
+        "memory": {"type": "string"}
269
+      },
270
+      "additionalProperties": false
271
+    },
272
+
273
+    "network": {
274
+      "id": "#/definitions/network",
275
+      "type": ["object", "null"],
276
+      "properties": {
277
+        "driver": {"type": "string"},
278
+        "driver_opts": {
279
+          "type": "object",
280
+          "patternProperties": {
281
+            "^.+$": {"type": ["string", "number"]}
282
+          }
283
+        },
284
+        "ipam": {
285
+          "type": "object",
286
+          "properties": {
287
+            "driver": {"type": "string"},
288
+            "config": {
289
+              "type": "array",
290
+              "items": {
291
+                "type": "object",
292
+                "properties": {
293
+                  "subnet": {"type": "string"}
294
+                },
295
+                "additionalProperties": false
296
+              }
297
+            }
298
+          },
299
+          "additionalProperties": false
300
+        },
301
+        "external": {
302
+          "type": ["boolean", "object"],
303
+          "properties": {
304
+            "name": {"type": "string"}
305
+          },
306
+          "additionalProperties": false
307
+        },
308
+        "labels": {"$ref": "#/definitions/list_or_dict"}
309
+      },
310
+      "additionalProperties": false
311
+    },
312
+
313
+    "volume": {
314
+      "id": "#/definitions/volume",
315
+      "type": ["object", "null"],
316
+      "properties": {
317
+        "driver": {"type": "string"},
318
+        "driver_opts": {
319
+          "type": "object",
320
+          "patternProperties": {
321
+            "^.+$": {"type": ["string", "number"]}
322
+          }
323
+        },
324
+        "external": {
325
+          "type": ["boolean", "object"],
326
+          "properties": {
327
+            "name": {"type": "string"}
328
+          }
329
+        }
330
+      },
331
+      "labels": {"$ref": "#/definitions/list_or_dict"},
332
+      "additionalProperties": false
333
+    },
334
+
335
+    "string_or_list": {
336
+      "oneOf": [
337
+        {"type": "string"},
338
+        {"$ref": "#/definitions/list_of_strings"}
339
+      ]
340
+    },
341
+
342
+    "list_of_strings": {
343
+      "type": "array",
344
+      "items": {"type": "string"},
345
+      "uniqueItems": true
346
+    },
347
+
348
+    "list_or_dict": {
349
+      "oneOf": [
350
+        {
351
+          "type": "object",
352
+          "patternProperties": {
353
+            ".+": {
354
+              "type": ["string", "number", "null"]
355
+            }
356
+          },
357
+          "additionalProperties": false
358
+        },
359
+        {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
360
+      ]
361
+    },
362
+
363
+    "constraints": {
364
+      "service": {
365
+        "id": "#/definitions/constraints/service",
366
+        "anyOf": [
367
+          {"required": ["build"]},
368
+          {"required": ["image"]}
369
+        ],
370
+        "properties": {
371
+          "build": {
372
+            "required": ["context"]
373
+          }
374
+        }
375
+      }
376
+    }
377
+  }
378
+}
0 379
new file mode 100644
... ...
@@ -0,0 +1,113 @@
0
+package schema
1
+
2
+//go:generate go-bindata -pkg schema -nometadata data
3
+
4
+import (
5
+	"fmt"
6
+	"strings"
7
+	"time"
8
+
9
+	"github.com/xeipuuv/gojsonschema"
10
+)
11
+
12
+type portsFormatChecker struct{}
13
+
14
+func (checker portsFormatChecker) IsFormat(input string) bool {
15
+	// TODO: implement this
16
+	return true
17
+}
18
+
19
+type durationFormatChecker struct{}
20
+
21
+func (checker durationFormatChecker) IsFormat(input string) bool {
22
+	_, err := time.ParseDuration(input)
23
+	return err == nil
24
+}
25
+
26
+func init() {
27
+	gojsonschema.FormatCheckers.Add("expose", portsFormatChecker{})
28
+	gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{})
29
+	gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{})
30
+}
31
+
32
+// Validate uses the jsonschema to validate the configuration
33
+func Validate(config map[string]interface{}) error {
34
+	schemaData, err := Asset("data/config_schema_v3.0.json")
35
+	if err != nil {
36
+		return err
37
+	}
38
+
39
+	schemaLoader := gojsonschema.NewStringLoader(string(schemaData))
40
+	dataLoader := gojsonschema.NewGoLoader(config)
41
+
42
+	result, err := gojsonschema.Validate(schemaLoader, dataLoader)
43
+	if err != nil {
44
+		return err
45
+	}
46
+
47
+	if !result.Valid() {
48
+		return toError(result)
49
+	}
50
+
51
+	return nil
52
+}
53
+
54
+func toError(result *gojsonschema.Result) error {
55
+	err := getMostSpecificError(result.Errors())
56
+	description := getDescription(err)
57
+	return fmt.Errorf("%s %s", err.Field(), description)
58
+}
59
+
60
+func getDescription(err gojsonschema.ResultError) string {
61
+	if err.Type() == "invalid_type" {
62
+		if expectedType, ok := err.Details()["expected"].(string); ok {
63
+			return fmt.Sprintf("must be a %s", humanReadableType(expectedType))
64
+		}
65
+	}
66
+
67
+	return err.Description()
68
+}
69
+
70
+func humanReadableType(definition string) string {
71
+	if definition[0:1] == "[" {
72
+		allTypes := strings.Split(definition[1:len(definition)-1], ",")
73
+		for i, t := range allTypes {
74
+			allTypes[i] = humanReadableType(t)
75
+		}
76
+		return fmt.Sprintf(
77
+			"%s or %s",
78
+			strings.Join(allTypes[0:len(allTypes)-1], ", "),
79
+			allTypes[len(allTypes)-1],
80
+		)
81
+	}
82
+	if definition == "object" {
83
+		return "mapping"
84
+	}
85
+	if definition == "array" {
86
+		return "list"
87
+	}
88
+	return definition
89
+}
90
+
91
+func getMostSpecificError(errors []gojsonschema.ResultError) gojsonschema.ResultError {
92
+	var mostSpecificError gojsonschema.ResultError
93
+
94
+	for _, err := range errors {
95
+		if mostSpecificError == nil {
96
+			mostSpecificError = err
97
+		} else if specificity(err) > specificity(mostSpecificError) {
98
+			mostSpecificError = err
99
+		} else if specificity(err) == specificity(mostSpecificError) {
100
+			// Invalid type errors win in a tie-breaker for most specific field name
101
+			if err.Type() == "invalid_type" && mostSpecificError.Type() != "invalid_type" {
102
+				mostSpecificError = err
103
+			}
104
+		}
105
+	}
106
+
107
+	return mostSpecificError
108
+}
109
+
110
+func specificity(err gojsonschema.ResultError) int {
111
+	return len(strings.Split(err.Field(), "."))
112
+}
0 113
new file mode 100644
... ...
@@ -0,0 +1,35 @@
0
+package schema
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/stretchr/testify/assert"
6
+)
7
+
8
+type dict map[string]interface{}
9
+
10
+func TestValid(t *testing.T) {
11
+	config := dict{
12
+		"version": "2.1",
13
+		"services": dict{
14
+			"foo": dict{
15
+				"image": "busybox",
16
+			},
17
+		},
18
+	}
19
+
20
+	assert.NoError(t, Validate(config))
21
+}
22
+
23
+func TestUndefinedTopLevelOption(t *testing.T) {
24
+	config := dict{
25
+		"version": "2.1",
26
+		"helicopters": dict{
27
+			"foo": dict{
28
+				"image": "busybox",
29
+			},
30
+		},
31
+	}
32
+
33
+	assert.Error(t, Validate(config))
34
+}
0 35
new file mode 100644
... ...
@@ -0,0 +1,100 @@
0
+package template
1
+
2
+import (
3
+	"fmt"
4
+	"regexp"
5
+	"strings"
6
+)
7
+
8
+var delimiter = "\\$"
9
+var substitution = "[_a-z][_a-z0-9]*(?::?-[^}]+)?"
10
+
11
+var patternString = fmt.Sprintf(
12
+	"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
13
+	delimiter, delimiter, substitution, substitution,
14
+)
15
+
16
+var pattern = regexp.MustCompile(patternString)
17
+
18
+// InvalidTemplateError is returned when a variable template is not in a valid
19
+// format
20
+type InvalidTemplateError struct {
21
+	Template string
22
+}
23
+
24
+func (e InvalidTemplateError) Error() string {
25
+	return fmt.Sprintf("Invalid template: %#v", e.Template)
26
+}
27
+
28
+// Mapping is a user-supplied function which maps from variable names to values.
29
+// Returns the value as a string and a bool indicating whether
30
+// the value is present, to distinguish between an empty string
31
+// and the absence of a value.
32
+type Mapping func(string) (string, bool)
33
+
34
+// Substitute variables in the string with their values
35
+func Substitute(template string, mapping Mapping) (result string, err *InvalidTemplateError) {
36
+	result = pattern.ReplaceAllStringFunc(template, func(substring string) string {
37
+		matches := pattern.FindStringSubmatch(substring)
38
+		groups := make(map[string]string)
39
+		for i, name := range pattern.SubexpNames() {
40
+			if i != 0 {
41
+				groups[name] = matches[i]
42
+			}
43
+		}
44
+
45
+		substitution := groups["named"]
46
+		if substitution == "" {
47
+			substitution = groups["braced"]
48
+		}
49
+		if substitution != "" {
50
+			// Soft default (fall back if unset or empty)
51
+			if strings.Contains(substitution, ":-") {
52
+				name, defaultValue := partition(substitution, ":-")
53
+				value, ok := mapping(name)
54
+				if !ok || value == "" {
55
+					return defaultValue
56
+				}
57
+				return value
58
+			}
59
+
60
+			// Hard default (fall back if-and-only-if empty)
61
+			if strings.Contains(substitution, "-") {
62
+				name, defaultValue := partition(substitution, "-")
63
+				value, ok := mapping(name)
64
+				if !ok {
65
+					return defaultValue
66
+				}
67
+				return value
68
+			}
69
+
70
+			// No default (fall back to empty string)
71
+			value, ok := mapping(substitution)
72
+			if !ok {
73
+				return ""
74
+			}
75
+			return value
76
+		}
77
+
78
+		if escaped := groups["escaped"]; escaped != "" {
79
+			return escaped
80
+		}
81
+
82
+		err = &InvalidTemplateError{Template: template}
83
+		return ""
84
+	})
85
+
86
+	return result, err
87
+}
88
+
89
+// Split the string at the first occurrence of sep, and return the part before the separator,
90
+// and the part after the separator.
91
+//
92
+// If the separator is not found, return the string itself, followed by an empty string.
93
+func partition(s, sep string) (string, string) {
94
+	if strings.Contains(s, sep) {
95
+		parts := strings.SplitN(s, sep, 2)
96
+		return parts[0], parts[1]
97
+	}
98
+	return s, ""
99
+}
0 100
new file mode 100644
... ...
@@ -0,0 +1,83 @@
0
+package template
1
+
2
+import (
3
+	"testing"
4
+
5
+	"github.com/stretchr/testify/assert"
6
+)
7
+
8
+var defaults = map[string]string{
9
+	"FOO": "first",
10
+	"BAR": "",
11
+}
12
+
13
+func defaultMapping(name string) (string, bool) {
14
+	val, ok := defaults[name]
15
+	return val, ok
16
+}
17
+
18
+func TestEscaped(t *testing.T) {
19
+	result, err := Substitute("$${foo}", defaultMapping)
20
+	assert.NoError(t, err)
21
+	assert.Equal(t, "${foo}", result)
22
+}
23
+
24
+func TestInvalid(t *testing.T) {
25
+	invalidTemplates := []string{
26
+		"${",
27
+		"$}",
28
+		"${}",
29
+		"${ }",
30
+		"${ foo}",
31
+		"${foo }",
32
+		"${foo!}",
33
+	}
34
+
35
+	for _, template := range invalidTemplates {
36
+		_, err := Substitute(template, defaultMapping)
37
+		assert.Error(t, err)
38
+		assert.IsType(t, &InvalidTemplateError{}, err)
39
+	}
40
+}
41
+
42
+func TestNoValueNoDefault(t *testing.T) {
43
+	for _, template := range []string{"This ${missing} var", "This ${BAR} var"} {
44
+		result, err := Substitute(template, defaultMapping)
45
+		assert.NoError(t, err)
46
+		assert.Equal(t, "This  var", result)
47
+	}
48
+}
49
+
50
+func TestValueNoDefault(t *testing.T) {
51
+	for _, template := range []string{"This $FOO var", "This ${FOO} var"} {
52
+		result, err := Substitute(template, defaultMapping)
53
+		assert.NoError(t, err)
54
+		assert.Equal(t, "This first var", result)
55
+	}
56
+}
57
+
58
+func TestNoValueWithDefault(t *testing.T) {
59
+	for _, template := range []string{"ok ${missing:-def}", "ok ${missing-def}"} {
60
+		result, err := Substitute(template, defaultMapping)
61
+		assert.NoError(t, err)
62
+		assert.Equal(t, "ok def", result)
63
+	}
64
+}
65
+
66
+func TestEmptyValueWithSoftDefault(t *testing.T) {
67
+	result, err := Substitute("ok ${BAR:-def}", defaultMapping)
68
+	assert.NoError(t, err)
69
+	assert.Equal(t, "ok def", result)
70
+}
71
+
72
+func TestEmptyValueWithHardDefault(t *testing.T) {
73
+	result, err := Substitute("ok ${BAR-def}", defaultMapping)
74
+	assert.NoError(t, err)
75
+	assert.Equal(t, "ok ", result)
76
+}
77
+
78
+func TestNonAlphanumericDefault(t *testing.T) {
79
+	result, err := Substitute("ok ${BAR:-/non:-alphanumeric}", defaultMapping)
80
+	assert.NoError(t, err)
81
+	assert.Equal(t, "ok /non:-alphanumeric", result)
82
+}
0 83
new file mode 100644
... ...
@@ -0,0 +1,232 @@
0
+package types
1
+
2
+import (
3
+	"time"
4
+)
5
+
6
+// UnsupportedProperties not yet supported by this implementation of the compose file
7
+var UnsupportedProperties = []string{
8
+	"build",
9
+	"cap_add",
10
+	"cap_drop",
11
+	"cgroup_parent",
12
+	"devices",
13
+	"dns",
14
+	"dns_search",
15
+	"domainname",
16
+	"external_links",
17
+	"ipc",
18
+	"links",
19
+	"mac_address",
20
+	"network_mode",
21
+	"privileged",
22
+	"read_only",
23
+	"restart",
24
+	"security_opt",
25
+	"shm_size",
26
+	"stop_signal",
27
+	"tmpfs",
28
+}
29
+
30
+// DeprecatedProperties that were removed from the v3 format, but their
31
+// use should not impact the behaviour of the application.
32
+var DeprecatedProperties = map[string]string{
33
+	"container_name": "Setting the container name is not supported.",
34
+	"expose":         "Exposing ports is unnecessary - services on the same network can access each other's containers on any port.",
35
+}
36
+
37
+// ForbiddenProperties that are not supported in this implementation of the
38
+// compose file.
39
+var ForbiddenProperties = map[string]string{
40
+	"extends":       "Support for `extends` is not implemented yet. Use `docker-compose config` to generate a configuration with all `extends` options resolved, and deploy from that.",
41
+	"volume_driver": "Instead of setting the volume driver on the service, define a volume using the top-level `volumes` option and specify the driver there.",
42
+	"volumes_from":  "To share a volume between services, define it using the top-level `volumes` option and reference it from each service that shares it using the service-level `volumes` option.",
43
+	"cpu_quota":     "Set resource limits using deploy.resources",
44
+	"cpu_shares":    "Set resource limits using deploy.resources",
45
+	"cpuset":        "Set resource limits using deploy.resources",
46
+	"mem_limit":     "Set resource limits using deploy.resources",
47
+	"memswap_limit": "Set resource limits using deploy.resources",
48
+}
49
+
50
+// Dict is a mapping of strings to interface{}
51
+type Dict map[string]interface{}
52
+
53
+// ConfigFile is a filename and the contents of the file as a Dict
54
+type ConfigFile struct {
55
+	Filename string
56
+	Config   Dict
57
+}
58
+
59
+// ConfigDetails are the details about a group of ConfigFiles
60
+type ConfigDetails struct {
61
+	WorkingDir  string
62
+	ConfigFiles []ConfigFile
63
+	Environment map[string]string
64
+}
65
+
66
+// Config is a full compose file configuration
67
+type Config struct {
68
+	Services []ServiceConfig
69
+	Networks map[string]NetworkConfig
70
+	Volumes  map[string]VolumeConfig
71
+}
72
+
73
+// ServiceConfig is the configuration of one service
74
+type ServiceConfig struct {
75
+	Name string
76
+
77
+	CapAdd          []string `mapstructure:"cap_add"`
78
+	CapDrop         []string `mapstructure:"cap_drop"`
79
+	CgroupParent    string   `mapstructure:"cgroup_parent"`
80
+	Command         []string `compose:"shell_command"`
81
+	ContainerName   string   `mapstructure:"container_name"`
82
+	DependsOn       []string `mapstructure:"depends_on"`
83
+	Deploy          DeployConfig
84
+	Devices         []string
85
+	DNS             []string          `compose:"string_or_list"`
86
+	DNSSearch       []string          `mapstructure:"dns_search" compose:"string_or_list"`
87
+	DomainName      string            `mapstructure:"domainname"`
88
+	Entrypoint      []string          `compose:"shell_command"`
89
+	Environment     map[string]string `compose:"list_or_dict_equals"`
90
+	Expose          []string          `compose:"list_of_strings_or_numbers"`
91
+	ExternalLinks   []string          `mapstructure:"external_links"`
92
+	ExtraHosts      map[string]string `mapstructure:"extra_hosts" compose:"list_or_dict_colon"`
93
+	Hostname        string
94
+	HealthCheck     *HealthCheckConfig
95
+	Image           string
96
+	Ipc             string
97
+	Labels          map[string]string `compose:"list_or_dict_equals"`
98
+	Links           []string
99
+	Logging         *LoggingConfig
100
+	MacAddress      string                           `mapstructure:"mac_address"`
101
+	NetworkMode     string                           `mapstructure:"network_mode"`
102
+	Networks        map[string]*ServiceNetworkConfig `compose:"list_or_struct_map"`
103
+	Pid             string
104
+	Ports           []string `compose:"list_of_strings_or_numbers"`
105
+	Privileged      bool
106
+	ReadOnly        bool `mapstructure:"read_only"`
107
+	Restart         string
108
+	SecurityOpt     []string       `mapstructure:"security_opt"`
109
+	StdinOpen       bool           `mapstructure:"stdin_open"`
110
+	StopGracePeriod *time.Duration `mapstructure:"stop_grace_period"`
111
+	StopSignal      string         `mapstructure:"stop_signal"`
112
+	Tmpfs           []string       `compose:"string_or_list"`
113
+	Tty             bool           `mapstructure:"tty"`
114
+	Ulimits         map[string]*UlimitsConfig
115
+	User            string
116
+	Volumes         []string
117
+	WorkingDir      string `mapstructure:"working_dir"`
118
+}
119
+
120
+// LoggingConfig the logging configuration for a service
121
+type LoggingConfig struct {
122
+	Driver  string
123
+	Options map[string]string
124
+}
125
+
126
+// DeployConfig the deployment configuration for a service
127
+type DeployConfig struct {
128
+	Mode          string
129
+	Replicas      *uint64
130
+	Labels        map[string]string `compose:"list_or_dict_equals"`
131
+	UpdateConfig  *UpdateConfig     `mapstructure:"update_config"`
132
+	Resources     Resources
133
+	RestartPolicy *RestartPolicy `mapstructure:"restart_policy"`
134
+	Placement     Placement
135
+}
136
+
137
+// HealthCheckConfig the healthcheck configuration for a service
138
+type HealthCheckConfig struct {
139
+	Test     []string `compose:"healthcheck"`
140
+	Timeout  string
141
+	Interval string
142
+	Retries  *uint64
143
+	Disable  bool
144
+}
145
+
146
+// UpdateConfig the service update configuration
147
+type UpdateConfig struct {
148
+	Parallelism     *uint64
149
+	Delay           time.Duration
150
+	FailureAction   string `mapstructure:"failure_action"`
151
+	Monitor         time.Duration
152
+	MaxFailureRatio float32 `mapstructure:"max_failure_ratio"`
153
+}
154
+
155
+// Resources the resource limits and reservations
156
+type Resources struct {
157
+	Limits       *Resource
158
+	Reservations *Resource
159
+}
160
+
161
+// Resource is a resource to be limited or reserved
162
+type Resource struct {
163
+	// TODO: types to convert from units and ratios
164
+	NanoCPUs    string    `mapstructure:"cpus"`
165
+	MemoryBytes UnitBytes `mapstructure:"memory"`
166
+}
167
+
168
+// UnitBytes is the bytes type
169
+type UnitBytes int64
170
+
171
+// RestartPolicy the service restart policy
172
+type RestartPolicy struct {
173
+	Condition   string
174
+	Delay       *time.Duration
175
+	MaxAttempts *uint64 `mapstructure:"max_attempts"`
176
+	Window      *time.Duration
177
+}
178
+
179
+// Placement constraints for the service
180
+type Placement struct {
181
+	Constraints []string
182
+}
183
+
184
+// ServiceNetworkConfig is the network configuration for a service
185
+type ServiceNetworkConfig struct {
186
+	Aliases     []string
187
+	Ipv4Address string `mapstructure:"ipv4_address"`
188
+	Ipv6Address string `mapstructure:"ipv6_address"`
189
+}
190
+
191
+// UlimitsConfig the ulimit configuration
192
+type UlimitsConfig struct {
193
+	Single int
194
+	Soft   int
195
+	Hard   int
196
+}
197
+
198
+// NetworkConfig for a network
199
+type NetworkConfig struct {
200
+	Driver     string
201
+	DriverOpts map[string]string `mapstructure:"driver_opts"`
202
+	Ipam       IPAMConfig
203
+	External   External
204
+	Labels     map[string]string `compose:"list_or_dict_equals"`
205
+}
206
+
207
+// IPAMConfig for a network
208
+type IPAMConfig struct {
209
+	Driver string
210
+	Config []*IPAMPool
211
+}
212
+
213
+// IPAMPool for a network
214
+type IPAMPool struct {
215
+	Subnet string
216
+}
217
+
218
+// VolumeConfig for a volume
219
+type VolumeConfig struct {
220
+	Driver     string
221
+	DriverOpts map[string]string `mapstructure:"driver_opts"`
222
+	External   External
223
+	Labels     map[string]string `compose:"list_or_dict_equals"`
224
+}
225
+
226
+// External identifies a Volume or Network as a reference to a resource that is
227
+// not managed, and should already exist.
228
+type External struct {
229
+	Name     string
230
+	External bool
231
+}
... ...
@@ -6,3 +6,4 @@ CONTAINERD_COMMIT=03e5862ec0d8d3b3f750e19fca3ee367e13c090e
6 6
 TINI_COMMIT=949e6facb77383876aeff8a6944dde66b3089574
7 7
 LIBNETWORK_COMMIT=0f534354b813003a754606689722fe253101bc4e
8 8
 VNDR_COMMIT=f56bd4504b4fad07a357913687fb652ee54bb3b0
9
+BINDATA_COMMIT=a0ff2567cfb70903282db057e799fd826784d41d
... ...
@@ -46,6 +46,14 @@ install_proxy() {
46 46
 	go build -ldflags="$PROXY_LDFLAGS" -o /usr/local/bin/docker-proxy github.com/docker/libnetwork/cmd/proxy
47 47
 }
48 48
 
49
+install_bindata() {
50
+    echo "Install go-bindata version $BINDATA_COMMIT"
51
+    git clone https://github.com/jteeuwen/go-bindata "$GOPATH/src/github.com/jteeuwen/go-bindata"
52
+    cd $GOPATH/src/github.com/jteeuwen/go-bindata
53
+    git checkout -q "$BINDATA_COMMIT"
54
+	go build -o /usr/local/bin/go-bindata github.com/jteeuwen/go-bindata/go-bindata
55
+}
56
+
49 57
 for prog in "$@"
50 58
 do
51 59
 	case $prog in
... ...
@@ -99,6 +107,10 @@ do
99 99
 			go build -v -o /usr/local/bin/vndr .
100 100
 			;;
101 101
 
102
+        bindata)
103
+            install_bindata
104
+            ;;
105
+
102 106
 		*)
103 107
 			echo echo "Usage: $0 [tomlv|runc|containerd|tini|proxy]"
104 108
 			exit 1
... ...
@@ -4,7 +4,7 @@ export SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 4
 source "${SCRIPTDIR}/.validate"
5 5
 
6 6
 IFS=$'\n'
7
-files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/' | grep -v '^api/types/container/' || true) )
7
+files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/' | grep -v '^api/types/container/' | grep -v '^cli/compose/schema/bindata.go' || true) )
8 8
 unset IFS
9 9
 
10 10
 errors=()
... ...
@@ -132,7 +132,6 @@ github.com/flynn-archive/go-shlex 3f9db97f856818214da2e1057f8ad84803971cff
132 132
 github.com/docker/go-metrics 86138d05f285fd9737a99bee2d9be30866b59d72
133 133
 
134 134
 # composefile
135
-github.com/aanand/compose-file a3e58764f50597b6217fec07e9bff7225c4a1719
136 135
 github.com/mitchellh/mapstructure f3009df150dadf309fdee4a54ed65c124afad715
137 136
 github.com/xeipuuv/gojsonpointer e0fe6f68307607d540ed8eac07a342c33fa1b54a
138 137
 github.com/xeipuuv/gojsonreference e02fc20de94c78484cd5ffb007f8af96be030a45
139 138
deleted file mode 100644
... ...
@@ -1,191 +0,0 @@
1
-
2
-                                 Apache License
3
-                           Version 2.0, January 2004
4
-                        https://www.apache.org/licenses/
5
-
6
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
-
8
-   1. Definitions.
9
-
10
-      "License" shall mean the terms and conditions for use, reproduction,
11
-      and distribution as defined by Sections 1 through 9 of this document.
12
-
13
-      "Licensor" shall mean the copyright owner or entity authorized by
14
-      the copyright owner that is granting the License.
15
-
16
-      "Legal Entity" shall mean the union of the acting entity and all
17
-      other entities that control, are controlled by, or are under common
18
-      control with that entity. For the purposes of this definition,
19
-      "control" means (i) the power, direct or indirect, to cause the
20
-      direction or management of such entity, whether by contract or
21
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
-      outstanding shares, or (iii) beneficial ownership of such entity.
23
-
24
-      "You" (or "Your") shall mean an individual or Legal Entity
25
-      exercising permissions granted by this License.
26
-
27
-      "Source" form shall mean the preferred form for making modifications,
28
-      including but not limited to software source code, documentation
29
-      source, and configuration files.
30
-
31
-      "Object" form shall mean any form resulting from mechanical
32
-      transformation or translation of a Source form, including but
33
-      not limited to compiled object code, generated documentation,
34
-      and conversions to other media types.
35
-
36
-      "Work" shall mean the work of authorship, whether in Source or
37
-      Object form, made available under the License, as indicated by a
38
-      copyright notice that is included in or attached to the work
39
-      (an example is provided in the Appendix below).
40
-
41
-      "Derivative Works" shall mean any work, whether in Source or Object
42
-      form, that is based on (or derived from) the Work and for which the
43
-      editorial revisions, annotations, elaborations, or other modifications
44
-      represent, as a whole, an original work of authorship. For the purposes
45
-      of this License, Derivative Works shall not include works that remain
46
-      separable from, or merely link (or bind by name) to the interfaces of,
47
-      the Work and Derivative Works thereof.
48
-
49
-      "Contribution" shall mean any work of authorship, including
50
-      the original version of the Work and any modifications or additions
51
-      to that Work or Derivative Works thereof, that is intentionally
52
-      submitted to Licensor for inclusion in the Work by the copyright owner
53
-      or by an individual or Legal Entity authorized to submit on behalf of
54
-      the copyright owner. For the purposes of this definition, "submitted"
55
-      means any form of electronic, verbal, or written communication sent
56
-      to the Licensor or its representatives, including but not limited to
57
-      communication on electronic mailing lists, source code control systems,
58
-      and issue tracking systems that are managed by, or on behalf of, the
59
-      Licensor for the purpose of discussing and improving the Work, but
60
-      excluding communication that is conspicuously marked or otherwise
61
-      designated in writing by the copyright owner as "Not a Contribution."
62
-
63
-      "Contributor" shall mean Licensor and any individual or Legal Entity
64
-      on behalf of whom a Contribution has been received by Licensor and
65
-      subsequently incorporated within the Work.
66
-
67
-   2. Grant of Copyright License. Subject to the terms and conditions of
68
-      this License, each Contributor hereby grants to You a perpetual,
69
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
-      copyright license to reproduce, prepare Derivative Works of,
71
-      publicly display, publicly perform, sublicense, and distribute the
72
-      Work and such Derivative Works in Source or Object form.
73
-
74
-   3. Grant of Patent License. Subject to the terms and conditions of
75
-      this License, each Contributor hereby grants to You a perpetual,
76
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
-      (except as stated in this section) patent license to make, have made,
78
-      use, offer to sell, sell, import, and otherwise transfer the Work,
79
-      where such license applies only to those patent claims licensable
80
-      by such Contributor that are necessarily infringed by their
81
-      Contribution(s) alone or by combination of their Contribution(s)
82
-      with the Work to which such Contribution(s) was submitted. If You
83
-      institute patent litigation against any entity (including a
84
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
85
-      or a Contribution incorporated within the Work constitutes direct
86
-      or contributory patent infringement, then any patent licenses
87
-      granted to You under this License for that Work shall terminate
88
-      as of the date such litigation is filed.
89
-
90
-   4. Redistribution. You may reproduce and distribute copies of the
91
-      Work or Derivative Works thereof in any medium, with or without
92
-      modifications, and in Source or Object form, provided that You
93
-      meet the following conditions:
94
-
95
-      (a) You must give any other recipients of the Work or
96
-          Derivative Works a copy of this License; and
97
-
98
-      (b) You must cause any modified files to carry prominent notices
99
-          stating that You changed the files; and
100
-
101
-      (c) You must retain, in the Source form of any Derivative Works
102
-          that You distribute, all copyright, patent, trademark, and
103
-          attribution notices from the Source form of the Work,
104
-          excluding those notices that do not pertain to any part of
105
-          the Derivative Works; and
106
-
107
-      (d) If the Work includes a "NOTICE" text file as part of its
108
-          distribution, then any Derivative Works that You distribute must
109
-          include a readable copy of the attribution notices contained
110
-          within such NOTICE file, excluding those notices that do not
111
-          pertain to any part of the Derivative Works, in at least one
112
-          of the following places: within a NOTICE text file distributed
113
-          as part of the Derivative Works; within the Source form or
114
-          documentation, if provided along with the Derivative Works; or,
115
-          within a display generated by the Derivative Works, if and
116
-          wherever such third-party notices normally appear. The contents
117
-          of the NOTICE file are for informational purposes only and
118
-          do not modify the License. You may add Your own attribution
119
-          notices within Derivative Works that You distribute, alongside
120
-          or as an addendum to the NOTICE text from the Work, provided
121
-          that such additional attribution notices cannot be construed
122
-          as modifying the License.
123
-
124
-      You may add Your own copyright statement to Your modifications and
125
-      may provide additional or different license terms and conditions
126
-      for use, reproduction, or distribution of Your modifications, or
127
-      for any such Derivative Works as a whole, provided Your use,
128
-      reproduction, and distribution of the Work otherwise complies with
129
-      the conditions stated in this License.
130
-
131
-   5. Submission of Contributions. Unless You explicitly state otherwise,
132
-      any Contribution intentionally submitted for inclusion in the Work
133
-      by You to the Licensor shall be under the terms and conditions of
134
-      this License, without any additional terms or conditions.
135
-      Notwithstanding the above, nothing herein shall supersede or modify
136
-      the terms of any separate license agreement you may have executed
137
-      with Licensor regarding such Contributions.
138
-
139
-   6. Trademarks. This License does not grant permission to use the trade
140
-      names, trademarks, service marks, or product names of the Licensor,
141
-      except as required for reasonable and customary use in describing the
142
-      origin of the Work and reproducing the content of the NOTICE file.
143
-
144
-   7. Disclaimer of Warranty. Unless required by applicable law or
145
-      agreed to in writing, Licensor provides the Work (and each
146
-      Contributor provides its Contributions) on an "AS IS" BASIS,
147
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
-      implied, including, without limitation, any warranties or conditions
149
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
-      PARTICULAR PURPOSE. You are solely responsible for determining the
151
-      appropriateness of using or redistributing the Work and assume any
152
-      risks associated with Your exercise of permissions under this License.
153
-
154
-   8. Limitation of Liability. In no event and under no legal theory,
155
-      whether in tort (including negligence), contract, or otherwise,
156
-      unless required by applicable law (such as deliberate and grossly
157
-      negligent acts) or agreed to in writing, shall any Contributor be
158
-      liable to You for damages, including any direct, indirect, special,
159
-      incidental, or consequential damages of any character arising as a
160
-      result of this License or out of the use or inability to use the
161
-      Work (including but not limited to damages for loss of goodwill,
162
-      work stoppage, computer failure or malfunction, or any and all
163
-      other commercial damages or losses), even if such Contributor
164
-      has been advised of the possibility of such damages.
165
-
166
-   9. Accepting Warranty or Additional Liability. While redistributing
167
-      the Work or Derivative Works thereof, You may choose to offer,
168
-      and charge a fee for, acceptance of support, warranty, indemnity,
169
-      or other liability obligations and/or rights consistent with this
170
-      License. However, in accepting such obligations, You may act only
171
-      on Your own behalf and on Your sole responsibility, not on behalf
172
-      of any other Contributor, and only if You agree to indemnify,
173
-      defend, and hold each Contributor harmless for any liability
174
-      incurred by, or claims asserted against, such Contributor by reason
175
-      of your accepting any such warranty or additional liability.
176
-
177
-   END OF TERMS AND CONDITIONS
178
-
179
-   Copyright 2016 Docker, Inc.
180
-
181
-   Licensed under the Apache License, Version 2.0 (the "License");
182
-   you may not use this file except in compliance with the License.
183
-   You may obtain a copy of the License at
184
-
185
-       https://www.apache.org/licenses/LICENSE-2.0
186
-
187
-   Unless required by applicable law or agreed to in writing, software
188
-   distributed under the License is distributed on an "AS IS" BASIS,
189
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190
-   See the License for the specific language governing permissions and
191
-   limitations under the License.
192 1
deleted file mode 100644
... ...
@@ -1,89 +0,0 @@
1
-package interpolation
2
-
3
-import (
4
-	"fmt"
5
-
6
-	"github.com/aanand/compose-file/template"
7
-	"github.com/aanand/compose-file/types"
8
-)
9
-
10
-func Interpolate(config types.Dict, section string, mapping template.Mapping) (types.Dict, error) {
11
-	out := types.Dict{}
12
-
13
-	for name, item := range config {
14
-		if item == nil {
15
-			out[name] = nil
16
-			continue
17
-		}
18
-		interpolatedItem, err := interpolateSectionItem(name, item.(types.Dict), section, mapping)
19
-		if err != nil {
20
-			return nil, err
21
-		}
22
-		out[name] = interpolatedItem
23
-	}
24
-
25
-	return out, nil
26
-}
27
-
28
-func interpolateSectionItem(
29
-	name string,
30
-	item types.Dict,
31
-	section string,
32
-	mapping template.Mapping,
33
-) (types.Dict, error) {
34
-
35
-	out := types.Dict{}
36
-
37
-	for key, value := range item {
38
-		interpolatedValue, err := recursiveInterpolate(value, mapping)
39
-		if err != nil {
40
-			return nil, fmt.Errorf(
41
-				"Invalid interpolation format for %#v option in %s %#v: %#v",
42
-				key, section, name, err.Template,
43
-			)
44
-		}
45
-		out[key] = interpolatedValue
46
-	}
47
-
48
-	return out, nil
49
-
50
-}
51
-
52
-func recursiveInterpolate(
53
-	value interface{},
54
-	mapping template.Mapping,
55
-) (interface{}, *template.InvalidTemplateError) {
56
-
57
-	switch value := value.(type) {
58
-
59
-	case string:
60
-		return template.Substitute(value, mapping)
61
-
62
-	case types.Dict:
63
-		out := types.Dict{}
64
-		for key, elem := range value {
65
-			interpolatedElem, err := recursiveInterpolate(elem, mapping)
66
-			if err != nil {
67
-				return nil, err
68
-			}
69
-			out[key] = interpolatedElem
70
-		}
71
-		return out, nil
72
-
73
-	case []interface{}:
74
-		out := make([]interface{}, len(value))
75
-		for i, elem := range value {
76
-			interpolatedElem, err := recursiveInterpolate(elem, mapping)
77
-			if err != nil {
78
-				return nil, err
79
-			}
80
-			out[i] = interpolatedElem
81
-		}
82
-		return out, nil
83
-
84
-	default:
85
-		return value, nil
86
-
87
-	}
88
-
89
-}
90 1
deleted file mode 100644
... ...
@@ -1,605 +0,0 @@
1
-package loader
2
-
3
-import (
4
-	"fmt"
5
-	"os"
6
-	"path"
7
-	"reflect"
8
-	"regexp"
9
-	"sort"
10
-	"strings"
11
-
12
-	"github.com/aanand/compose-file/interpolation"
13
-	"github.com/aanand/compose-file/schema"
14
-	"github.com/aanand/compose-file/types"
15
-	"github.com/docker/docker/runconfig/opts"
16
-	units "github.com/docker/go-units"
17
-	shellwords "github.com/mattn/go-shellwords"
18
-	"github.com/mitchellh/mapstructure"
19
-	yaml "gopkg.in/yaml.v2"
20
-)
21
-
22
-var (
23
-	fieldNameRegexp = regexp.MustCompile("[A-Z][a-z0-9]+")
24
-)
25
-
26
-// ParseYAML reads the bytes from a file, parses the bytes into a mapping
27
-// structure, and returns it.
28
-func ParseYAML(source []byte) (types.Dict, error) {
29
-	var cfg interface{}
30
-	if err := yaml.Unmarshal(source, &cfg); err != nil {
31
-		return nil, err
32
-	}
33
-	cfgMap, ok := cfg.(map[interface{}]interface{})
34
-	if !ok {
35
-		return nil, fmt.Errorf("Top-level object must be a mapping")
36
-	}
37
-	converted, err := convertToStringKeysRecursive(cfgMap, "")
38
-	if err != nil {
39
-		return nil, err
40
-	}
41
-	return converted.(types.Dict), nil
42
-}
43
-
44
-// Load reads a ConfigDetails and returns a fully loaded configuration
45
-func Load(configDetails types.ConfigDetails) (*types.Config, error) {
46
-	if len(configDetails.ConfigFiles) < 1 {
47
-		return nil, fmt.Errorf("No files specified")
48
-	}
49
-	if len(configDetails.ConfigFiles) > 1 {
50
-		return nil, fmt.Errorf("Multiple files are not yet supported")
51
-	}
52
-
53
-	configDict := getConfigDict(configDetails)
54
-
55
-	if services, ok := configDict["services"]; ok {
56
-		if servicesDict, ok := services.(types.Dict); ok {
57
-			forbidden := getProperties(servicesDict, types.ForbiddenProperties)
58
-
59
-			if len(forbidden) > 0 {
60
-				return nil, &ForbiddenPropertiesError{Properties: forbidden}
61
-			}
62
-		}
63
-	}
64
-
65
-	if err := schema.Validate(configDict); err != nil {
66
-		return nil, err
67
-	}
68
-
69
-	cfg := types.Config{}
70
-	version := configDict["version"].(string)
71
-	if version != "3" && version != "3.0" {
72
-		return nil, fmt.Errorf(`Unsupported Compose file version: %#v. The only version supported is "3" (or "3.0")`, version)
73
-	}
74
-
75
-	if services, ok := configDict["services"]; ok {
76
-		servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", os.LookupEnv)
77
-		if err != nil {
78
-			return nil, err
79
-		}
80
-
81
-		servicesList, err := loadServices(servicesConfig, configDetails.WorkingDir)
82
-		if err != nil {
83
-			return nil, err
84
-		}
85
-
86
-		cfg.Services = servicesList
87
-	}
88
-
89
-	if networks, ok := configDict["networks"]; ok {
90
-		networksConfig, err := interpolation.Interpolate(networks.(types.Dict), "network", os.LookupEnv)
91
-		if err != nil {
92
-			return nil, err
93
-		}
94
-
95
-		networksMapping, err := loadNetworks(networksConfig)
96
-		if err != nil {
97
-			return nil, err
98
-		}
99
-
100
-		cfg.Networks = networksMapping
101
-	}
102
-
103
-	if volumes, ok := configDict["volumes"]; ok {
104
-		volumesConfig, err := interpolation.Interpolate(volumes.(types.Dict), "volume", os.LookupEnv)
105
-		if err != nil {
106
-			return nil, err
107
-		}
108
-
109
-		volumesMapping, err := loadVolumes(volumesConfig)
110
-		if err != nil {
111
-			return nil, err
112
-		}
113
-
114
-		cfg.Volumes = volumesMapping
115
-	}
116
-
117
-	return &cfg, nil
118
-}
119
-
120
-func GetUnsupportedProperties(configDetails types.ConfigDetails) []string {
121
-	unsupported := map[string]bool{}
122
-
123
-	for _, service := range getServices(getConfigDict(configDetails)) {
124
-		serviceDict := service.(types.Dict)
125
-		for _, property := range types.UnsupportedProperties {
126
-			if _, isSet := serviceDict[property]; isSet {
127
-				unsupported[property] = true
128
-			}
129
-		}
130
-	}
131
-
132
-	return sortedKeys(unsupported)
133
-}
134
-
135
-func sortedKeys(set map[string]bool) []string {
136
-	var keys []string
137
-	for key := range set {
138
-		keys = append(keys, key)
139
-	}
140
-	sort.Strings(keys)
141
-	return keys
142
-}
143
-
144
-func GetDeprecatedProperties(configDetails types.ConfigDetails) map[string]string {
145
-	return getProperties(getServices(getConfigDict(configDetails)), types.DeprecatedProperties)
146
-}
147
-
148
-func getProperties(services types.Dict, propertyMap map[string]string) map[string]string {
149
-	output := map[string]string{}
150
-
151
-	for _, service := range services {
152
-		if serviceDict, ok := service.(types.Dict); ok {
153
-			for property, description := range propertyMap {
154
-				if _, isSet := serviceDict[property]; isSet {
155
-					output[property] = description
156
-				}
157
-			}
158
-		}
159
-	}
160
-
161
-	return output
162
-}
163
-
164
-type ForbiddenPropertiesError struct {
165
-	Properties map[string]string
166
-}
167
-
168
-func (e *ForbiddenPropertiesError) Error() string {
169
-	return "Configuration contains forbidden properties"
170
-}
171
-
172
-// TODO: resolve multiple files into a single config
173
-func getConfigDict(configDetails types.ConfigDetails) types.Dict {
174
-	return configDetails.ConfigFiles[0].Config
175
-}
176
-
177
-func getServices(configDict types.Dict) types.Dict {
178
-	if services, ok := configDict["services"]; ok {
179
-		if servicesDict, ok := services.(types.Dict); ok {
180
-			return servicesDict
181
-		}
182
-	}
183
-
184
-	return types.Dict{}
185
-}
186
-
187
-func transform(source map[string]interface{}, target interface{}) error {
188
-	data := mapstructure.Metadata{}
189
-	config := &mapstructure.DecoderConfig{
190
-		DecodeHook: mapstructure.ComposeDecodeHookFunc(
191
-			transformHook,
192
-			mapstructure.StringToTimeDurationHookFunc()),
193
-		Result:   target,
194
-		Metadata: &data,
195
-	}
196
-	decoder, err := mapstructure.NewDecoder(config)
197
-	if err != nil {
198
-		return err
199
-	}
200
-	err = decoder.Decode(source)
201
-	// TODO: log unused keys
202
-	return err
203
-}
204
-
205
-func transformHook(
206
-	source reflect.Type,
207
-	target reflect.Type,
208
-	data interface{},
209
-) (interface{}, error) {
210
-	switch target {
211
-	case reflect.TypeOf(types.External{}):
212
-		return transformExternal(source, target, data)
213
-	case reflect.TypeOf(make(map[string]string, 0)):
214
-		return transformMapStringString(source, target, data)
215
-	case reflect.TypeOf(types.UlimitsConfig{}):
216
-		return transformUlimits(source, target, data)
217
-	case reflect.TypeOf(types.UnitBytes(0)):
218
-		return loadSize(data)
219
-	}
220
-	switch target.Kind() {
221
-	case reflect.Struct:
222
-		return transformStruct(source, target, data)
223
-	}
224
-	return data, nil
225
-}
226
-
227
-// keys needs to be converted to strings for jsonschema
228
-// TODO: don't use types.Dict
229
-func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) {
230
-	if mapping, ok := value.(map[interface{}]interface{}); ok {
231
-		dict := make(types.Dict)
232
-		for key, entry := range mapping {
233
-			str, ok := key.(string)
234
-			if !ok {
235
-				var location string
236
-				if keyPrefix == "" {
237
-					location = "at top level"
238
-				} else {
239
-					location = fmt.Sprintf("in %s", keyPrefix)
240
-				}
241
-				return nil, fmt.Errorf("Non-string key %s: %#v", location, key)
242
-			}
243
-			var newKeyPrefix string
244
-			if keyPrefix == "" {
245
-				newKeyPrefix = str
246
-			} else {
247
-				newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str)
248
-			}
249
-			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
250
-			if err != nil {
251
-				return nil, err
252
-			}
253
-			dict[str] = convertedEntry
254
-		}
255
-		return dict, nil
256
-	}
257
-	if list, ok := value.([]interface{}); ok {
258
-		var convertedList []interface{}
259
-		for index, entry := range list {
260
-			newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index)
261
-			convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
262
-			if err != nil {
263
-				return nil, err
264
-			}
265
-			convertedList = append(convertedList, convertedEntry)
266
-		}
267
-		return convertedList, nil
268
-	}
269
-	return value, nil
270
-}
271
-
272
-func loadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceConfig, error) {
273
-	var services []types.ServiceConfig
274
-
275
-	for name, serviceDef := range servicesDict {
276
-		serviceConfig, err := loadService(name, serviceDef.(types.Dict), workingDir)
277
-		if err != nil {
278
-			return nil, err
279
-		}
280
-		services = append(services, *serviceConfig)
281
-	}
282
-
283
-	return services, nil
284
-}
285
-
286
-func loadService(name string, serviceDict types.Dict, workingDir string) (*types.ServiceConfig, error) {
287
-	serviceConfig := &types.ServiceConfig{}
288
-	if err := transform(serviceDict, serviceConfig); err != nil {
289
-		return nil, err
290
-	}
291
-	serviceConfig.Name = name
292
-
293
-	if err := resolveEnvironment(serviceConfig, serviceDict, workingDir); err != nil {
294
-		return nil, err
295
-	}
296
-
297
-	if err := resolveVolumePaths(serviceConfig.Volumes, workingDir); err != nil {
298
-		return nil, err
299
-	}
300
-
301
-	return serviceConfig, nil
302
-}
303
-
304
-func resolveEnvironment(serviceConfig *types.ServiceConfig, serviceDict types.Dict, workingDir string) error {
305
-	environment := make(map[string]string)
306
-
307
-	if envFileVal, ok := serviceDict["env_file"]; ok {
308
-		envFiles := loadStringOrListOfStrings(envFileVal)
309
-
310
-		var envVars []string
311
-
312
-		for _, file := range envFiles {
313
-			filePath := path.Join(workingDir, file)
314
-			fileVars, err := opts.ParseEnvFile(filePath)
315
-			if err != nil {
316
-				return err
317
-			}
318
-			envVars = append(envVars, fileVars...)
319
-		}
320
-
321
-		for k, v := range opts.ConvertKVStringsToMap(envVars) {
322
-			environment[k] = v
323
-		}
324
-	}
325
-
326
-	for k, v := range serviceConfig.Environment {
327
-		environment[k] = v
328
-	}
329
-
330
-	serviceConfig.Environment = environment
331
-
332
-	return nil
333
-}
334
-
335
-func resolveVolumePaths(volumes []string, workingDir string) error {
336
-	for i, mapping := range volumes {
337
-		parts := strings.SplitN(mapping, ":", 2)
338
-		if len(parts) == 1 {
339
-			continue
340
-		}
341
-
342
-		if strings.HasPrefix(parts[0], ".") {
343
-			parts[0] = path.Join(workingDir, parts[0])
344
-		}
345
-		parts[0] = expandUser(parts[0])
346
-
347
-		volumes[i] = strings.Join(parts, ":")
348
-	}
349
-
350
-	return nil
351
-}
352
-
353
-// TODO: make this more robust
354
-func expandUser(path string) string {
355
-	if strings.HasPrefix(path, "~") {
356
-		return strings.Replace(path, "~", os.Getenv("HOME"), 1)
357
-	}
358
-	return path
359
-}
360
-
361
-func transformUlimits(
362
-	source reflect.Type,
363
-	target reflect.Type,
364
-	data interface{},
365
-) (interface{}, error) {
366
-	switch value := data.(type) {
367
-	case int:
368
-		return types.UlimitsConfig{Single: value}, nil
369
-	case types.Dict:
370
-		ulimit := types.UlimitsConfig{}
371
-		ulimit.Soft = value["soft"].(int)
372
-		ulimit.Hard = value["hard"].(int)
373
-		return ulimit, nil
374
-	default:
375
-		return data, fmt.Errorf("invalid type %T for ulimits", value)
376
-	}
377
-}
378
-
379
-func loadNetworks(source types.Dict) (map[string]types.NetworkConfig, error) {
380
-	networks := make(map[string]types.NetworkConfig)
381
-	err := transform(source, &networks)
382
-	if err != nil {
383
-		return networks, err
384
-	}
385
-	for name, network := range networks {
386
-		if network.External.External && network.External.Name == "" {
387
-			network.External.Name = name
388
-			networks[name] = network
389
-		}
390
-	}
391
-	return networks, nil
392
-}
393
-
394
-func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) {
395
-	volumes := make(map[string]types.VolumeConfig)
396
-	err := transform(source, &volumes)
397
-	if err != nil {
398
-		return volumes, err
399
-	}
400
-	for name, volume := range volumes {
401
-		if volume.External.External && volume.External.Name == "" {
402
-			volume.External.Name = name
403
-			volumes[name] = volume
404
-		}
405
-	}
406
-	return volumes, nil
407
-}
408
-
409
-func transformStruct(
410
-	source reflect.Type,
411
-	target reflect.Type,
412
-	data interface{},
413
-) (interface{}, error) {
414
-	structValue, ok := data.(map[string]interface{})
415
-	if !ok {
416
-		// FIXME: this is necessary because of convertToStringKeysRecursive
417
-		structValue, ok = data.(types.Dict)
418
-		if !ok {
419
-			panic(fmt.Sprintf(
420
-				"transformStruct called with non-map type: %T, %s", data, data))
421
-		}
422
-	}
423
-
424
-	var err error
425
-	for i := 0; i < target.NumField(); i++ {
426
-		field := target.Field(i)
427
-		fieldTag := field.Tag.Get("compose")
428
-
429
-		yamlName := toYAMLName(field.Name)
430
-		value, ok := structValue[yamlName]
431
-		if !ok {
432
-			continue
433
-		}
434
-
435
-		structValue[yamlName], err = convertField(
436
-			fieldTag, reflect.TypeOf(value), field.Type, value)
437
-		if err != nil {
438
-			return nil, fmt.Errorf("field %s: %s", yamlName, err.Error())
439
-		}
440
-	}
441
-	return structValue, nil
442
-}
443
-
444
-func transformMapStringString(
445
-	source reflect.Type,
446
-	target reflect.Type,
447
-	data interface{},
448
-) (interface{}, error) {
449
-	switch value := data.(type) {
450
-	case map[string]interface{}:
451
-		return toMapStringString(value), nil
452
-	case types.Dict:
453
-		return toMapStringString(value), nil
454
-	case map[string]string:
455
-		return value, nil
456
-	default:
457
-		return data, fmt.Errorf("invalid type %T for map[string]string", value)
458
-	}
459
-}
460
-
461
-func convertField(
462
-	fieldTag string,
463
-	source reflect.Type,
464
-	target reflect.Type,
465
-	data interface{},
466
-) (interface{}, error) {
467
-	switch fieldTag {
468
-	case "":
469
-		return data, nil
470
-	case "healthcheck":
471
-		return loadHealthcheck(data)
472
-	case "list_or_dict_equals":
473
-		return loadMappingOrList(data, "="), nil
474
-	case "list_or_dict_colon":
475
-		return loadMappingOrList(data, ":"), nil
476
-	case "list_or_struct_map":
477
-		return loadListOrStructMap(data, target)
478
-	case "string_or_list":
479
-		return loadStringOrListOfStrings(data), nil
480
-	case "list_of_strings_or_numbers":
481
-		return loadListOfStringsOrNumbers(data), nil
482
-	case "shell_command":
483
-		return loadShellCommand(data)
484
-	case "size":
485
-		return loadSize(data)
486
-	case "-":
487
-		return nil, nil
488
-	}
489
-	return data, nil
490
-}
491
-
492
-func transformExternal(
493
-	source reflect.Type,
494
-	target reflect.Type,
495
-	data interface{},
496
-) (interface{}, error) {
497
-	switch value := data.(type) {
498
-	case bool:
499
-		return map[string]interface{}{"external": value}, nil
500
-	case types.Dict:
501
-		return map[string]interface{}{"external": true, "name": value["name"]}, nil
502
-	case map[string]interface{}:
503
-		return map[string]interface{}{"external": true, "name": value["name"]}, nil
504
-	default:
505
-		return data, fmt.Errorf("invalid type %T for external", value)
506
-	}
507
-}
508
-
509
-func toYAMLName(name string) string {
510
-	nameParts := fieldNameRegexp.FindAllString(name, -1)
511
-	for i, p := range nameParts {
512
-		nameParts[i] = strings.ToLower(p)
513
-	}
514
-	return strings.Join(nameParts, "_")
515
-}
516
-
517
-func loadListOrStructMap(value interface{}, target reflect.Type) (interface{}, error) {
518
-	if list, ok := value.([]interface{}); ok {
519
-		mapValue := map[interface{}]interface{}{}
520
-		for _, name := range list {
521
-			mapValue[name] = nil
522
-		}
523
-		return mapValue, nil
524
-	}
525
-
526
-	return value, nil
527
-}
528
-
529
-func loadListOfStringsOrNumbers(value interface{}) []string {
530
-	list := value.([]interface{})
531
-	result := make([]string, len(list))
532
-	for i, item := range list {
533
-		result[i] = fmt.Sprint(item)
534
-	}
535
-	return result
536
-}
537
-
538
-func loadStringOrListOfStrings(value interface{}) []string {
539
-	if list, ok := value.([]interface{}); ok {
540
-		result := make([]string, len(list))
541
-		for i, item := range list {
542
-			result[i] = fmt.Sprint(item)
543
-		}
544
-		return result
545
-	}
546
-	return []string{value.(string)}
547
-}
548
-
549
-func loadMappingOrList(mappingOrList interface{}, sep string) map[string]string {
550
-	if mapping, ok := mappingOrList.(types.Dict); ok {
551
-		return toMapStringString(mapping)
552
-	}
553
-	if list, ok := mappingOrList.([]interface{}); ok {
554
-		result := make(map[string]string)
555
-		for _, value := range list {
556
-			parts := strings.SplitN(value.(string), sep, 2)
557
-			if len(parts) == 1 {
558
-				result[parts[0]] = ""
559
-			} else {
560
-				result[parts[0]] = parts[1]
561
-			}
562
-		}
563
-		return result
564
-	}
565
-	panic(fmt.Errorf("expected a map or a slice, got: %#v", mappingOrList))
566
-}
567
-
568
-func loadShellCommand(value interface{}) (interface{}, error) {
569
-	if str, ok := value.(string); ok {
570
-		return shellwords.Parse(str)
571
-	}
572
-	return value, nil
573
-}
574
-
575
-func loadHealthcheck(value interface{}) (interface{}, error) {
576
-	if str, ok := value.(string); ok {
577
-		return append([]string{"CMD-SHELL"}, str), nil
578
-	}
579
-	return value, nil
580
-}
581
-
582
-func loadSize(value interface{}) (int64, error) {
583
-	switch value := value.(type) {
584
-	case int:
585
-		return int64(value), nil
586
-	case string:
587
-		return units.RAMInBytes(value)
588
-	}
589
-	panic(fmt.Errorf("invalid type for size %T", value))
590
-}
591
-
592
-func toMapStringString(value map[string]interface{}) map[string]string {
593
-	output := make(map[string]string)
594
-	for key, value := range value {
595
-		output[key] = toString(value)
596
-	}
597
-	return output
598
-}
599
-
600
-func toString(value interface{}) string {
601
-	if value == nil {
602
-		return ""
603
-	}
604
-	return fmt.Sprint(value)
605
-}
606 1
deleted file mode 100644
... ...
@@ -1,237 +0,0 @@
1
-// Code generated by go-bindata.
2
-// sources:
3
-// data/config_schema_v3.0.json
4
-// DO NOT EDIT!
5
-
6
-package schema
7
-
8
-import (
9
-	"bytes"
10
-	"compress/gzip"
11
-	"fmt"
12
-	"io"
13
-	"io/ioutil"
14
-	"os"
15
-	"path/filepath"
16
-	"strings"
17
-	"time"
18
-)
19
-
20
-func bindataRead(data []byte, name string) ([]byte, error) {
21
-	gz, err := gzip.NewReader(bytes.NewBuffer(data))
22
-	if err != nil {
23
-		return nil, fmt.Errorf("Read %q: %v", name, err)
24
-	}
25
-
26
-	var buf bytes.Buffer
27
-	_, err = io.Copy(&buf, gz)
28
-	clErr := gz.Close()
29
-
30
-	if err != nil {
31
-		return nil, fmt.Errorf("Read %q: %v", name, err)
32
-	}
33
-	if clErr != nil {
34
-		return nil, err
35
-	}
36
-
37
-	return buf.Bytes(), nil
38
-}
39
-
40
-type asset struct {
41
-	bytes []byte
42
-	info  os.FileInfo
43
-}
44
-
45
-type bindataFileInfo struct {
46
-	name    string
47
-	size    int64
48
-	mode    os.FileMode
49
-	modTime time.Time
50
-}
51
-
52
-func (fi bindataFileInfo) Name() string {
53
-	return fi.name
54
-}
55
-func (fi bindataFileInfo) Size() int64 {
56
-	return fi.size
57
-}
58
-func (fi bindataFileInfo) Mode() os.FileMode {
59
-	return fi.mode
60
-}
61
-func (fi bindataFileInfo) ModTime() time.Time {
62
-	return fi.modTime
63
-}
64
-func (fi bindataFileInfo) IsDir() bool {
65
-	return false
66
-}
67
-func (fi bindataFileInfo) Sys() interface{} {
68
-	return nil
69
-}
70
-
71
-var _dataConfig_schema_v30Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\x4d\x93\xdb\x28\x13\xbe\xfb\x57\x4c\x29\xb9\xc5\x33\x93\xaa\x37\xf5\x56\x6d\x6e\x7b\xdc\xd3\xee\x79\x5d\x8a\x0a\x4b\xd8\x26\x23\x09\x02\xc8\x89\x93\xf2\x7f\x5f\x10\x92\x0c\x88\x2f\xdb\x4a\x66\x0f\x3b\x87\xa9\x19\xe8\x6e\xfa\xe3\xa1\x69\x1a\xfd\x58\x3d\x3c\x64\x6f\x59\x79\x80\x0d\xc8\x3e\x3e\x64\x07\xce\xc9\xc7\xe7\xe7\xcf\x0c\xb7\x8f\x6a\xf4\x09\xd3\xfd\x73\x45\xc1\x8e\x3f\xbe\xff\xf0\xac\xc6\xde\x64\x6b\xc9\x87\x2a\xc9\x52\xe2\x76\x87\xf6\x85\x9a\x29\x8e\xff\x7b\x7a\xff\x24\xd9\x15\x09\x3f\x11\x28\x89\xf0\xf6\x33\x2c\xb9\x1a\xa3\xf0\x4b\x87\x28\x94\xcc\x9b\xec\x08\x29\x43\x82\x3a\x5f\xaf\xe4\x1c\xa1\x98\x40\xca\x11\x64\x62\x56\x2a\x27\xc6\x46\x92\x71\x40\x13\xcb\x38\x45\xed\x3e\xeb\x87\xcf\xbd\x04\x31\xc9\x20\x3d\xa2\x52\x93\x30\xa9\xfa\xe6\xf9\x22\xff\x79\x22\x5b\xdb\x52\x35\x65\xfb\x71\x02\x38\x87\xb4\xfd\x6b\xae\x5b\x3f\xfd\x69\x03\x1e\xbf\xff\xfe\xf8\xf7\xfb\xc7\xdf\x9e\x8a\xc7\xfc\xdd\x5b\x63\x5a\xfa\x97\xc2\x9d\x5a\xbe\x82\x3b\xd4\x22\x2e\xac\x99\xd6\xcf\x26\xca\xf3\xf0\xd7\x79\x5a\x18\x54\x55\x4f\x0c\x6a\x63\xed\x1d\xa8\x19\x34\x6d\x6e\x21\xff\x8a\xe9\x4b\xcc\xe6\x89\xec\x95\x6c\x1e\xd6\x77\xd8\x6c\x9a\x73\xc4\x75\xd7\x44\x23\x38\x52\xbd\x92\x31\x6a\xf9\xfb\xe2\xb7\x1a\x8d\x0e\xd2\x2a\x0a\x6d\xed\x5e\x41\x03\xed\x2e\x57\xb9\xd0\xe6\xf7\xd5\xe4\x2c\x8f\x97\x2a\x48\x6a\x7c\x92\x63\x1e\x7f\x28\x82\x06\xb6\x3c\x9b\x5c\x20\xf8\xb6\x1d\xaa\x2b\xdb\xa3\xb8\x85\x7f\x4a\x11\x1b\x6d\xf0\x41\x48\xb6\x36\xb6\x26\xa7\x9f\x37\xfe\xf3\x07\x7c\x9a\xf7\xd8\x32\xcd\x8b\xdc\xc5\xe1\x37\xde\x1b\x15\x5e\x5a\xb9\x00\x97\x2f\x90\xee\x50\x0d\x53\x39\x00\xdd\xb3\x80\xcb\x6a\xc4\x78\x81\x69\x51\x21\xa1\xfd\xd9\x62\x9f\xc9\x8b\xe3\x69\x62\xd5\xfe\xcb\x57\x0e\x81\x59\x09\x48\x21\xc4\x19\x76\x00\x4a\xc1\x29\x5b\x0b\x00\x71\xd8\x30\xb7\x89\x0f\x59\xd7\xa2\x2f\x1d\xfc\x63\x20\xe1\xb4\x83\xb6\xdc\x4a\x28\xb7\xbc\xe0\x3d\xc5\x1d\x29\x08\xa0\x12\x60\x61\xf7\x8b\xb8\x36\x0d\x68\x97\x42\xdd\x35\x76\x24\x78\x5e\x60\x0e\xa0\x16\xd2\xa2\x05\x4d\x0c\x48\x72\xd7\xc1\xb6\x62\x85\x3a\xff\x82\x30\xda\x15\x8a\x9f\x59\x02\xa6\xc3\x70\xd1\x78\x54\x6d\x08\xd8\x4a\x8c\x84\xb6\xd4\x2d\xb3\x18\x0b\x06\x01\x2d\x0f\x37\xf2\xe3\x46\xb8\x2f\xc5\x77\x02\x28\xf4\x44\x30\x52\x78\xf9\xd7\x01\x01\xb6\xc7\x62\xca\x25\x57\xbb\x41\x70\x23\x8a\xdb\x66\xdc\x0d\x29\x09\x66\x4a\xf2\x92\xff\x1b\xc1\x0c\xda\x8e\xb1\x0c\xd4\xa7\x26\x53\x0d\x9f\x8c\x1c\x9b\xd1\x70\xe1\x94\xb6\x6b\xb6\x90\xca\x92\xce\xa0\xdc\x61\xda\x00\xa9\xec\xb8\xb6\x36\x6d\x78\xda\x81\x3c\xdd\x81\xba\x0d\xf2\x58\x07\xb5\xf0\x4e\xfb\xb2\x3c\xc4\x85\x78\x0a\x8a\x03\x66\x3c\x3d\x87\x6b\xec\x07\x08\x6a\x7e\x10\x65\x71\xf9\x12\x60\xd7\xa9\x0c\x6e\xb1\x6c\x0a\xc8\x51\x03\xf6\x71\x22\x52\xc6\x48\x6a\xb0\x85\xf5\x4d\x76\x2e\xea\x7c\x4d\x2c\xde\xef\x25\xa9\x0f\x71\xb3\xca\x65\x98\x8e\x9d\xf9\x15\x45\xe2\x46\x91\x7a\x80\x63\x72\x29\xb8\xec\xc9\x78\x01\xa2\x14\x0a\x56\x9f\x06\xe9\xa7\x27\x55\x7c\x06\x76\x55\xff\x57\x5d\x67\xb9\x5d\x2e\xc8\x9f\xf9\x98\x39\x62\x59\x98\x56\x50\x18\x51\x69\x40\x29\xeb\x06\x0a\x99\x27\xae\x17\xd2\xa1\xd8\x2f\x1a\x5c\xf9\x00\x3a\x23\xb6\x7d\xe3\xcd\xd4\x57\x1f\x84\x3d\xdb\xd5\xf5\x63\x52\xe8\xa2\x17\x88\x88\x35\x3e\xf5\x52\xd5\xbc\xa8\x1b\x87\x58\x4f\x07\x6a\x04\x18\x8c\x6f\x76\xaf\x23\x0d\x69\x88\x1c\x3f\x24\x62\xc2\xc5\xfb\xff\x20\xaf\x87\xd5\x2b\x33\xbd\x46\x8e\x88\xba\xa8\xd2\x6f\x37\x97\x22\x79\x64\xb7\xfd\xe4\x12\x9e\xa0\xca\x9f\x2b\xfa\x0c\xa1\x6f\x30\x82\x29\x9f\xed\xae\x5f\x73\xdc\xab\xa5\xef\x3e\xed\x89\x48\xdc\xa2\x5c\xda\x43\xf3\xd6\xb2\xc5\xb8\x86\xa0\x35\x52\x0f\x85\xa0\x12\x25\x73\x7d\x4a\xa0\x64\x1c\xd0\xe8\x85\x82\xc1\xb2\xa3\x88\x9f\x0a\x71\x1e\x2c\x5e\x67\xb0\x43\x53\x30\xf4\x1d\x9a\xd1\xbc\xe4\xfb\x41\x50\x6e\xf0\xf0\x0a\xb5\x42\x1b\xd8\x46\x4d\x64\x1c\x13\x21\x7f\x2f\x30\x17\x35\x53\x92\xee\x29\x28\x61\x21\xb0\x89\x70\xe5\x62\x58\xeb\xb1\xad\x3a\x0a\x24\x9e\x0d\x31\xbc\x21\xbb\x1b\x6f\x07\x9c\xc7\x63\xd6\xd5\xa8\x41\x7e\x30\x3b\xb2\x64\x42\x22\x57\x49\xdc\x9d\xbb\x03\x79\xfb\xa2\xa9\xb8\x66\x08\x6c\x52\x57\xba\x0b\x94\x0e\xe1\xca\x21\xa1\x64\x38\x00\x6a\x46\x29\xa0\x47\xcf\xc0\xf0\x8e\xbb\x19\x5c\x05\x85\x53\x2f\xa3\x83\xdb\xcb\x5b\x0f\x8a\xe4\x4e\xfa\xab\x72\xb2\xad\x46\xee\x4d\x8b\x67\x67\x5a\xec\x58\xb4\xba\xd3\xfb\x8b\x8b\xee\x64\x59\xc2\x48\x64\x57\xc8\xad\xc2\xca\x52\xf7\x8a\x0e\xaf\x75\x9b\x18\x05\xb8\x7a\x7d\x3a\xa9\xdd\xef\xdb\x4c\x80\x1b\x4f\x89\x4b\x97\xd4\xd3\xf8\x93\xf8\xa0\x47\x23\x79\xb8\x7c\xca\x51\x03\x71\xc7\x23\x54\x14\x8a\x31\xcb\xf3\x43\xa6\x33\x84\x89\xb4\x9c\x5a\x0a\xfe\xd2\x4b\x7b\x85\x18\xd8\x5a\xfd\xbf\x29\x47\xdd\x14\x5e\x25\xf6\xd2\x3b\x8d\x04\x57\xa3\x5c\x20\xb6\x81\xda\x5c\x0b\x19\xa9\x51\x09\x58\x2c\xcb\xdc\x71\x85\xec\x48\x05\x38\x2c\xd4\x53\xd2\x55\x79\x3d\x90\xd0\x09\xa0\xa0\xae\xa1\x58\xb4\x49\x49\x90\x22\x06\x35\x38\xdd\x74\xe0\xf5\xec\x3b\x80\xea\x8e\xc2\x02\x94\x7c\x78\xad\x8a\x20\x53\x38\x5f\x38\x06\x3b\x33\x45\xda\x92\x0d\xf8\x56\x8c\xcb\xf6\x24\xce\x6d\xe5\x2d\xbc\x52\x6f\x7f\x1a\x12\x18\xee\x68\x39\x73\xf6\xcd\x21\xba\x1c\xe4\x1e\xc4\x8c\x2b\xce\x4c\x17\x13\x32\x29\x4d\x97\xf3\x28\x7f\xf4\xdc\x18\x2a\xc1\x82\x60\x81\xf6\xd3\x52\x16\x0a\x48\x2b\x27\xa7\x00\xe2\x4e\x04\x4a\x38\xc8\x3a\xa7\x21\x3c\xba\x59\x7b\x86\xaf\xa8\xad\xf0\xd7\x2b\x16\x5c\x0e\x4a\xa4\x16\x45\xa6\x95\xef\xee\x75\xb4\xd0\x1d\x08\x53\xaf\x3e\xd6\xef\x35\xeb\x8e\x53\x7d\xc2\x67\x24\xeb\x4f\x74\xf1\xb7\x4e\x4f\xa6\x2f\x49\x17\xed\xd8\x34\xb0\xc1\xd4\x09\xc0\x80\x8d\x89\x4f\xd3\x31\x0b\x47\xb2\x05\x0e\xb5\xa4\x0e\xdf\x40\x25\x2f\x74\x8b\xdf\x24\xe2\x5d\xbc\x3c\x9e\x8f\x10\x01\xcd\x52\x9b\x23\xb9\xe7\x99\x39\x8f\x60\x63\xed\x79\xaf\x40\xa9\xeb\xec\x17\xc4\xb4\x8e\xeb\x3e\x50\xb0\x6e\x2b\x10\x12\x82\xe6\xe5\xc7\xf9\x10\x9b\x7e\x05\x39\xfb\x2f\x1c\xf7\xe5\xbc\xf1\xb9\xc2\x13\xd5\xcd\x54\x48\xae\x27\x5f\xe5\xc9\x21\xf6\xbe\x15\x2c\xa7\xff\x95\xf5\xdd\x1d\x69\x71\xf8\xb4\x22\x92\x32\x06\xaa\xff\x32\xc6\x20\xe5\xf5\xf1\x15\x38\x13\x6f\xbc\x1c\x5c\x01\x1a\xab\xab\xa4\x81\x67\x7e\x73\x0c\xc5\x39\xb9\x27\x3e\x70\xe4\xa6\x1a\x36\xd9\xc7\xf9\x67\x6b\x66\x0a\x0d\x35\x1c\x46\x12\x4f\x8f\xd4\x5a\x74\x70\x5e\xd8\xf2\x05\x61\xfb\xf4\x2e\x70\x50\x84\xde\xae\x7e\x52\x86\x5d\xa0\x99\xe3\x8e\xa9\x55\x5c\x8e\xde\x9d\x7f\x7b\xe5\xc9\x54\x1a\xff\xec\x4b\x2c\x69\x67\x7b\x9a\x75\x36\x7e\x98\x5d\x36\xf5\x15\x55\x6e\xf8\xc7\x22\x51\x2f\xc1\x5a\x9e\xc8\xf5\x7a\xdb\x17\x46\xe7\xf7\x59\x76\x8f\x6f\xfc\x4e\x2a\x0f\x6f\xf6\xd5\xf8\xfb\xbc\x3a\xaf\xfe\x09\x00\x00\xff\xff\x37\x89\x5b\xf1\x5b\x2a\x00\x00")
72
-
73
-func dataConfig_schema_v30JsonBytes() ([]byte, error) {
74
-	return bindataRead(
75
-		_dataConfig_schema_v30Json,
76
-		"data/config_schema_v3.0.json",
77
-	)
78
-}
79
-
80
-func dataConfig_schema_v30Json() (*asset, error) {
81
-	bytes, err := dataConfig_schema_v30JsonBytes()
82
-	if err != nil {
83
-		return nil, err
84
-	}
85
-
86
-	info := bindataFileInfo{name: "data/config_schema_v3.0.json", size: 10843, mode: os.FileMode(420), modTime: time.Unix(1479392593, 0)}
87
-	a := &asset{bytes: bytes, info: info}
88
-	return a, nil
89
-}
90
-
91
-// Asset loads and returns the asset for the given name.
92
-// It returns an error if the asset could not be found or
93
-// could not be loaded.
94
-func Asset(name string) ([]byte, error) {
95
-	cannonicalName := strings.Replace(name, "\\", "/", -1)
96
-	if f, ok := _bindata[cannonicalName]; ok {
97
-		a, err := f()
98
-		if err != nil {
99
-			return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
100
-		}
101
-		return a.bytes, nil
102
-	}
103
-	return nil, fmt.Errorf("Asset %s not found", name)
104
-}
105
-
106
-// MustAsset is like Asset but panics when Asset would return an error.
107
-// It simplifies safe initialization of global variables.
108
-func MustAsset(name string) []byte {
109
-	a, err := Asset(name)
110
-	if err != nil {
111
-		panic("asset: Asset(" + name + "): " + err.Error())
112
-	}
113
-
114
-	return a
115
-}
116
-
117
-// AssetInfo loads and returns the asset info for the given name.
118
-// It returns an error if the asset could not be found or
119
-// could not be loaded.
120
-func AssetInfo(name string) (os.FileInfo, error) {
121
-	cannonicalName := strings.Replace(name, "\\", "/", -1)
122
-	if f, ok := _bindata[cannonicalName]; ok {
123
-		a, err := f()
124
-		if err != nil {
125
-			return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
126
-		}
127
-		return a.info, nil
128
-	}
129
-	return nil, fmt.Errorf("AssetInfo %s not found", name)
130
-}
131
-
132
-// AssetNames returns the names of the assets.
133
-func AssetNames() []string {
134
-	names := make([]string, 0, len(_bindata))
135
-	for name := range _bindata {
136
-		names = append(names, name)
137
-	}
138
-	return names
139
-}
140
-
141
-// _bindata is a table, holding each asset generator, mapped to its name.
142
-var _bindata = map[string]func() (*asset, error){
143
-	"data/config_schema_v3.0.json": dataConfig_schema_v30Json,
144
-}
145
-
146
-// AssetDir returns the file names below a certain
147
-// directory embedded in the file by go-bindata.
148
-// For example if you run go-bindata on data/... and data contains the
149
-// following hierarchy:
150
-//     data/
151
-//       foo.txt
152
-//       img/
153
-//         a.png
154
-//         b.png
155
-// then AssetDir("data") would return []string{"foo.txt", "img"}
156
-// AssetDir("data/img") would return []string{"a.png", "b.png"}
157
-// AssetDir("foo.txt") and AssetDir("notexist") would return an error
158
-// AssetDir("") will return []string{"data"}.
159
-func AssetDir(name string) ([]string, error) {
160
-	node := _bintree
161
-	if len(name) != 0 {
162
-		cannonicalName := strings.Replace(name, "\\", "/", -1)
163
-		pathList := strings.Split(cannonicalName, "/")
164
-		for _, p := range pathList {
165
-			node = node.Children[p]
166
-			if node == nil {
167
-				return nil, fmt.Errorf("Asset %s not found", name)
168
-			}
169
-		}
170
-	}
171
-	if node.Func != nil {
172
-		return nil, fmt.Errorf("Asset %s not found", name)
173
-	}
174
-	rv := make([]string, 0, len(node.Children))
175
-	for childName := range node.Children {
176
-		rv = append(rv, childName)
177
-	}
178
-	return rv, nil
179
-}
180
-
181
-type bintree struct {
182
-	Func     func() (*asset, error)
183
-	Children map[string]*bintree
184
-}
185
-var _bintree = &bintree{nil, map[string]*bintree{
186
-	"data": &bintree{nil, map[string]*bintree{
187
-		"config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}},
188
-	}},
189
-}}
190
-
191
-// RestoreAsset restores an asset under the given directory
192
-func RestoreAsset(dir, name string) error {
193
-	data, err := Asset(name)
194
-	if err != nil {
195
-		return err
196
-	}
197
-	info, err := AssetInfo(name)
198
-	if err != nil {
199
-		return err
200
-	}
201
-	err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
202
-	if err != nil {
203
-		return err
204
-	}
205
-	err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
206
-	if err != nil {
207
-		return err
208
-	}
209
-	err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
210
-	if err != nil {
211
-		return err
212
-	}
213
-	return nil
214
-}
215
-
216
-// RestoreAssets restores an asset under the given directory recursively
217
-func RestoreAssets(dir, name string) error {
218
-	children, err := AssetDir(name)
219
-	// File
220
-	if err != nil {
221
-		return RestoreAsset(dir, name)
222
-	}
223
-	// Dir
224
-	for _, child := range children {
225
-		err = RestoreAssets(dir, filepath.Join(name, child))
226
-		if err != nil {
227
-			return err
228
-		}
229
-	}
230
-	return nil
231
-}
232
-
233
-func _filePath(dir, name string) string {
234
-	cannonicalName := strings.Replace(name, "\\", "/", -1)
235
-	return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
236
-}
237
-
238 1
deleted file mode 100644
... ...
@@ -1,113 +0,0 @@
1
-package schema
2
-
3
-//go:generate go-bindata -pkg schema data
4
-
5
-import (
6
-	"fmt"
7
-	"strings"
8
-	"time"
9
-
10
-	"github.com/xeipuuv/gojsonschema"
11
-)
12
-
13
-type portsFormatChecker struct{}
14
-
15
-func (checker portsFormatChecker) IsFormat(input string) bool {
16
-	// TODO: implement this
17
-	return true
18
-}
19
-
20
-type durationFormatChecker struct{}
21
-
22
-func (checker durationFormatChecker) IsFormat(input string) bool {
23
-	_, err := time.ParseDuration(input)
24
-	return err == nil
25
-}
26
-
27
-func init() {
28
-	gojsonschema.FormatCheckers.Add("expose", portsFormatChecker{})
29
-	gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{})
30
-	gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{})
31
-}
32
-
33
-// Validate uses the jsonschema to validate the configuration
34
-func Validate(config map[string]interface{}) error {
35
-	schemaData, err := Asset("data/config_schema_v3.0.json")
36
-	if err != nil {
37
-		return err
38
-	}
39
-
40
-	schemaLoader := gojsonschema.NewStringLoader(string(schemaData))
41
-	dataLoader := gojsonschema.NewGoLoader(config)
42
-
43
-	result, err := gojsonschema.Validate(schemaLoader, dataLoader)
44
-	if err != nil {
45
-		return err
46
-	}
47
-
48
-	if !result.Valid() {
49
-		return toError(result)
50
-	}
51
-
52
-	return nil
53
-}
54
-
55
-func toError(result *gojsonschema.Result) error {
56
-	err := getMostSpecificError(result.Errors())
57
-	description := getDescription(err)
58
-	return fmt.Errorf("%s %s", err.Field(), description)
59
-}
60
-
61
-func getDescription(err gojsonschema.ResultError) string {
62
-	if err.Type() == "invalid_type" {
63
-		if expectedType, ok := err.Details()["expected"].(string); ok {
64
-			return fmt.Sprintf("must be a %s", humanReadableType(expectedType))
65
-		}
66
-	}
67
-
68
-	return err.Description()
69
-}
70
-
71
-func humanReadableType(definition string) string {
72
-	if definition[0:1] == "[" {
73
-		allTypes := strings.Split(definition[1:len(definition)-1], ",")
74
-		for i, t := range allTypes {
75
-			allTypes[i] = humanReadableType(t)
76
-		}
77
-		return fmt.Sprintf(
78
-			"%s or %s",
79
-			strings.Join(allTypes[0:len(allTypes)-1], ", "),
80
-			allTypes[len(allTypes)-1],
81
-		)
82
-	}
83
-	if definition == "object" {
84
-		return "mapping"
85
-	}
86
-	if definition == "array" {
87
-		return "list"
88
-	}
89
-	return definition
90
-}
91
-
92
-func getMostSpecificError(errors []gojsonschema.ResultError) gojsonschema.ResultError {
93
-	var mostSpecificError gojsonschema.ResultError
94
-
95
-	for _, err := range errors {
96
-		if mostSpecificError == nil {
97
-			mostSpecificError = err
98
-		} else if specificity(err) > specificity(mostSpecificError) {
99
-			mostSpecificError = err
100
-		} else if specificity(err) == specificity(mostSpecificError) {
101
-			// Invalid type errors win in a tie-breaker for most specific field name
102
-			if err.Type() == "invalid_type" && mostSpecificError.Type() != "invalid_type" {
103
-				mostSpecificError = err
104
-			}
105
-		}
106
-	}
107
-
108
-	return mostSpecificError
109
-}
110
-
111
-func specificity(err gojsonschema.ResultError) int {
112
-	return len(strings.Split(err.Field(), "."))
113
-}
114 1
deleted file mode 100644
... ...
@@ -1,108 +0,0 @@
1
-package template
2
-
3
-import (
4
-	"fmt"
5
-	"regexp"
6
-	"strings"
7
-)
8
-
9
-var delimiter = "\\$"
10
-var substitution = "[_a-z][_a-z0-9]*(?::?-[^}]+)?"
11
-
12
-var patternString = fmt.Sprintf(
13
-	"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
14
-	delimiter, delimiter, substitution, substitution,
15
-)
16
-
17
-var pattern = regexp.MustCompile(patternString)
18
-
19
-type InvalidTemplateError struct {
20
-	Template string
21
-}
22
-
23
-func (e InvalidTemplateError) Error() string {
24
-	return fmt.Sprintf("Invalid template: %#v", e.Template)
25
-}
26
-
27
-// A user-supplied function which maps from variable names to values.
28
-// Returns the value as a string and a bool indicating whether
29
-// the value is present, to distinguish between an empty string
30
-// and the absence of a value.
31
-type Mapping func(string) (string, bool)
32
-
33
-func Substitute(template string, mapping Mapping) (result string, err *InvalidTemplateError) {
34
-	defer func() {
35
-		if r := recover(); r != nil {
36
-			if e, ok := r.(*InvalidTemplateError); ok {
37
-				err = e
38
-			} else {
39
-				panic(r)
40
-			}
41
-		}
42
-	}()
43
-
44
-	result = pattern.ReplaceAllStringFunc(template, func(substring string) string {
45
-		matches := pattern.FindStringSubmatch(substring)
46
-		groups := make(map[string]string)
47
-		for i, name := range pattern.SubexpNames() {
48
-			if i != 0 {
49
-				groups[name] = matches[i]
50
-			}
51
-		}
52
-
53
-		substitution := groups["named"]
54
-		if substitution == "" {
55
-			substitution = groups["braced"]
56
-		}
57
-		if substitution != "" {
58
-			// Soft default (fall back if unset or empty)
59
-			if strings.Contains(substitution, ":-") {
60
-				name, defaultValue := partition(substitution, ":-")
61
-				value, ok := mapping(name)
62
-				if !ok || value == "" {
63
-					return defaultValue
64
-				}
65
-				return value
66
-			}
67
-
68
-			// Hard default (fall back if-and-only-if empty)
69
-			if strings.Contains(substitution, "-") {
70
-				name, defaultValue := partition(substitution, "-")
71
-				value, ok := mapping(name)
72
-				if !ok {
73
-					return defaultValue
74
-				}
75
-				return value
76
-			}
77
-
78
-			// No default (fall back to empty string)
79
-			value, ok := mapping(substitution)
80
-			if !ok {
81
-				return ""
82
-			}
83
-			return value
84
-		}
85
-
86
-		if escaped := groups["escaped"]; escaped != "" {
87
-			return escaped
88
-		}
89
-
90
-		panic(&InvalidTemplateError{Template: template})
91
-		return ""
92
-	})
93
-
94
-	return
95
-}
96
-
97
-// Split the string at the first occurrence of sep, and return the part before the separator,
98
-// and the part after the separator.
99
-//
100
-// If the separator is not found, return the string itself, followed by an empty string.
101
-func partition(s, sep string) (string, string) {
102
-	if strings.Contains(s, sep) {
103
-		parts := strings.SplitN(s, sep, 2)
104
-		return parts[0], parts[1]
105
-	} else {
106
-		return s, ""
107
-	}
108
-}
109 1
deleted file mode 100644
... ...
@@ -1,207 +0,0 @@
1
-package types
2
-
3
-import (
4
-	"time"
5
-)
6
-
7
-var UnsupportedProperties = []string{
8
-	"build",
9
-	"cap_add",
10
-	"cap_drop",
11
-	"cgroup_parent",
12
-	"devices",
13
-	"dns",
14
-	"dns_search",
15
-	"domainname",
16
-	"external_links",
17
-	"ipc",
18
-	"links",
19
-	"mac_address",
20
-	"network_mode",
21
-	"privileged",
22
-	"read_only",
23
-	"restart",
24
-	"security_opt",
25
-	"shm_size",
26
-	"stop_signal",
27
-	"tmpfs",
28
-}
29
-
30
-var DeprecatedProperties = map[string]string{
31
-	"container_name": "Setting the container name is not supported.",
32
-	"expose":         "Exposing ports is unnecessary - services on the same network can access each other's containers on any port.",
33
-}
34
-
35
-var ForbiddenProperties = map[string]string{
36
-	"extends":       "Support for `extends` is not implemented yet. Use `docker-compose config` to generate a configuration with all `extends` options resolved, and deploy from that.",
37
-	"volume_driver": "Instead of setting the volume driver on the service, define a volume using the top-level `volumes` option and specify the driver there.",
38
-	"volumes_from":  "To share a volume between services, define it using the top-level `volumes` option and reference it from each service that shares it using the service-level `volumes` option.",
39
-	"cpu_quota":     "Set resource limits using deploy.resources",
40
-	"cpu_shares":    "Set resource limits using deploy.resources",
41
-	"cpuset":        "Set resource limits using deploy.resources",
42
-	"mem_limit":     "Set resource limits using deploy.resources",
43
-	"memswap_limit": "Set resource limits using deploy.resources",
44
-}
45
-
46
-type Dict map[string]interface{}
47
-
48
-type ConfigFile struct {
49
-	Filename string
50
-	Config   Dict
51
-}
52
-
53
-type ConfigDetails struct {
54
-	WorkingDir  string
55
-	ConfigFiles []ConfigFile
56
-	Environment map[string]string
57
-}
58
-
59
-type Config struct {
60
-	Services []ServiceConfig
61
-	Networks map[string]NetworkConfig
62
-	Volumes  map[string]VolumeConfig
63
-}
64
-
65
-type ServiceConfig struct {
66
-	Name string
67
-
68
-	CapAdd          []string `mapstructure:"cap_add"`
69
-	CapDrop         []string `mapstructure:"cap_drop"`
70
-	CgroupParent    string   `mapstructure:"cgroup_parent"`
71
-	Command         []string `compose:"shell_command"`
72
-	ContainerName   string   `mapstructure:"container_name"`
73
-	DependsOn       []string `mapstructure:"depends_on"`
74
-	Deploy          DeployConfig
75
-	Devices         []string
76
-	Dns             []string          `compose:"string_or_list"`
77
-	DnsSearch       []string          `mapstructure:"dns_search" compose:"string_or_list"`
78
-	DomainName      string            `mapstructure:"domainname"`
79
-	Entrypoint      []string          `compose:"shell_command"`
80
-	Environment     map[string]string `compose:"list_or_dict_equals"`
81
-	Expose          []string          `compose:"list_of_strings_or_numbers"`
82
-	ExternalLinks   []string          `mapstructure:"external_links"`
83
-	ExtraHosts      map[string]string `mapstructure:"extra_hosts" compose:"list_or_dict_colon"`
84
-	Hostname        string
85
-	HealthCheck     *HealthCheckConfig
86
-	Image           string
87
-	Ipc             string
88
-	Labels          map[string]string `compose:"list_or_dict_equals"`
89
-	Links           []string
90
-	Logging         *LoggingConfig
91
-	MacAddress      string                           `mapstructure:"mac_address"`
92
-	NetworkMode     string                           `mapstructure:"network_mode"`
93
-	Networks        map[string]*ServiceNetworkConfig `compose:"list_or_struct_map"`
94
-	Pid             string
95
-	Ports           []string `compose:"list_of_strings_or_numbers"`
96
-	Privileged      bool
97
-	ReadOnly        bool `mapstructure:"read_only"`
98
-	Restart         string
99
-	SecurityOpt     []string       `mapstructure:"security_opt"`
100
-	StdinOpen       bool           `mapstructure:"stdin_open"`
101
-	StopGracePeriod *time.Duration `mapstructure:"stop_grace_period"`
102
-	StopSignal      string         `mapstructure:"stop_signal"`
103
-	Tmpfs           []string       `compose:"string_or_list"`
104
-	Tty             bool           `mapstructure:"tty"`
105
-	Ulimits         map[string]*UlimitsConfig
106
-	User            string
107
-	Volumes         []string
108
-	WorkingDir      string `mapstructure:"working_dir"`
109
-}
110
-
111
-type LoggingConfig struct {
112
-	Driver  string
113
-	Options map[string]string
114
-}
115
-
116
-type DeployConfig struct {
117
-	Mode          string
118
-	Replicas      *uint64
119
-	Labels        map[string]string `compose:"list_or_dict_equals"`
120
-	UpdateConfig  *UpdateConfig     `mapstructure:"update_config"`
121
-	Resources     Resources
122
-	RestartPolicy *RestartPolicy `mapstructure:"restart_policy"`
123
-	Placement     Placement
124
-}
125
-
126
-type HealthCheckConfig struct {
127
-	Test     []string `compose:"healthcheck"`
128
-	Timeout  string
129
-	Interval string
130
-	Retries  *uint64
131
-	Disable  bool
132
-}
133
-
134
-type UpdateConfig struct {
135
-	Parallelism     *uint64
136
-	Delay           time.Duration
137
-	FailureAction   string `mapstructure:"failure_action"`
138
-	Monitor         time.Duration
139
-	MaxFailureRatio float32 `mapstructure:"max_failure_ratio"`
140
-}
141
-
142
-type Resources struct {
143
-	Limits       *Resource
144
-	Reservations *Resource
145
-}
146
-
147
-type Resource struct {
148
-	// TODO: types to convert from units and ratios
149
-	NanoCPUs    string    `mapstructure:"cpus"`
150
-	MemoryBytes UnitBytes `mapstructure:"memory"`
151
-}
152
-
153
-type UnitBytes int64
154
-
155
-type RestartPolicy struct {
156
-	Condition   string
157
-	Delay       *time.Duration
158
-	MaxAttempts *uint64 `mapstructure:"max_attempts"`
159
-	Window      *time.Duration
160
-}
161
-
162
-type Placement struct {
163
-	Constraints []string
164
-}
165
-
166
-type ServiceNetworkConfig struct {
167
-	Aliases     []string
168
-	Ipv4Address string `mapstructure:"ipv4_address"`
169
-	Ipv6Address string `mapstructure:"ipv6_address"`
170
-}
171
-
172
-type UlimitsConfig struct {
173
-	Single int
174
-	Soft   int
175
-	Hard   int
176
-}
177
-
178
-type NetworkConfig struct {
179
-	Driver     string
180
-	DriverOpts map[string]string `mapstructure:"driver_opts"`
181
-	Ipam       IPAMConfig
182
-	External   External
183
-	Labels     map[string]string `compose:"list_or_dict_equals"`
184
-}
185
-
186
-type IPAMConfig struct {
187
-	Driver string
188
-	Config []*IPAMPool
189
-}
190
-
191
-type IPAMPool struct {
192
-	Subnet string
193
-}
194
-
195
-type VolumeConfig struct {
196
-	Driver     string
197
-	DriverOpts map[string]string `mapstructure:"driver_opts"`
198
-	External   External
199
-	Labels     map[string]string `compose:"list_or_dict_equals"`
200
-}
201
-
202
-// External identifies a Volume or Network as a reference to a resource that is
203
-// not managed, and should already exist.
204
-type External struct {
205
-	Name     string
206
-	External bool
207
-}