Browse Source

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 8 years ago
parent
commit
f5af9b9738

+ 1 - 1
Dockerfile

@@ -239,7 +239,7 @@ RUN ./contrib/download-frozen-image-v2.sh /docker-frozen-images \
 # Please edit hack/dockerfile/install-binaries.sh to update them.
 COPY hack/dockerfile/binaries-commits /tmp/binaries-commits
 COPY hack/dockerfile/install-binaries.sh /tmp/install-binaries.sh
-RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy
+RUN /tmp/install-binaries.sh tomlv vndr runc containerd tini proxy bindata
 
 # Wrap all commands in the "docker-in-docker" script to allow nested containers
 ENTRYPOINT ["hack/dind"]

+ 2 - 2
cli/command/stack/deploy.go

@@ -11,13 +11,13 @@ import (
 	"github.com/spf13/cobra"
 	"golang.org/x/net/context"
 
-	"github.com/aanand/compose-file/loader"
-	composetypes "github.com/aanand/compose-file/types"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/cli/compose/convert"
+	"github.com/docker/docker/cli/compose/loader"
+	composetypes "github.com/docker/docker/cli/compose/types"
 	dockerclient "github.com/docker/docker/client"
 )
 

+ 1 - 1
cli/compose/convert/compose.go

@@ -1,9 +1,9 @@
 package convert
 
 import (
-	composetypes "github.com/aanand/compose-file/types"
 	"github.com/docker/docker/api/types"
 	networktypes "github.com/docker/docker/api/types/network"
+	composetypes "github.com/docker/docker/cli/compose/types"
 )
 
 const (

+ 1 - 1
cli/compose/convert/compose_test.go

@@ -3,9 +3,9 @@ package convert
 import (
 	"testing"
 
-	composetypes "github.com/aanand/compose-file/types"
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/network"
+	composetypes "github.com/docker/docker/cli/compose/types"
 	"github.com/docker/docker/pkg/testutil/assert"
 )
 

+ 1 - 1
cli/compose/convert/service.go

@@ -4,9 +4,9 @@ import (
 	"fmt"
 	"time"
 
-	composetypes "github.com/aanand/compose-file/types"
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/swarm"
+	composetypes "github.com/docker/docker/cli/compose/types"
 	"github.com/docker/docker/opts"
 	runconfigopts "github.com/docker/docker/runconfig/opts"
 	"github.com/docker/go-connections/nat"

+ 1 - 1
cli/compose/convert/service_test.go

@@ -6,9 +6,9 @@ import (
 	"testing"
 	"time"
 
-	composetypes "github.com/aanand/compose-file/types"
 	"github.com/docker/docker/api/types/container"
 	"github.com/docker/docker/api/types/swarm"
+	composetypes "github.com/docker/docker/cli/compose/types"
 	"github.com/docker/docker/pkg/testutil/assert"
 )
 

+ 1 - 1
cli/compose/convert/volume.go

@@ -4,8 +4,8 @@ import (
 	"fmt"
 	"strings"
 
-	composetypes "github.com/aanand/compose-file/types"
 	"github.com/docker/docker/api/types/mount"
+	composetypes "github.com/docker/docker/cli/compose/types"
 )
 
 type volumes map[string]composetypes.VolumeConfig

+ 1 - 1
cli/compose/convert/volume_test.go

@@ -3,8 +3,8 @@ package convert
 import (
 	"testing"
 
-	composetypes "github.com/aanand/compose-file/types"
 	"github.com/docker/docker/api/types/mount"
+	composetypes "github.com/docker/docker/cli/compose/types"
 	"github.com/docker/docker/pkg/testutil/assert"
 )
 

+ 3 - 2
vendor/github.com/aanand/compose-file/interpolation/interpolation.go → cli/compose/interpolation/interpolation.go

@@ -3,10 +3,11 @@ package interpolation
 import (
 	"fmt"
 
-	"github.com/aanand/compose-file/template"
-	"github.com/aanand/compose-file/types"
+	"github.com/docker/docker/cli/compose/template"
+	"github.com/docker/docker/cli/compose/types"
 )
 
+// Interpolate replaces variables in a string with the values from a mapping
 func Interpolate(config types.Dict, section string, mapping template.Mapping) (types.Dict, error) {
 	out := types.Dict{}
 

+ 59 - 0
cli/compose/interpolation/interpolation_test.go

@@ -0,0 +1,59 @@
+package interpolation
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/docker/docker/cli/compose/types"
+)
+
+var defaults = map[string]string{
+	"USER": "jenny",
+	"FOO":  "bar",
+}
+
+func defaultMapping(name string) (string, bool) {
+	val, ok := defaults[name]
+	return val, ok
+}
+
+func TestInterpolate(t *testing.T) {
+	services := types.Dict{
+		"servicea": types.Dict{
+			"image":   "example:${USER}",
+			"volumes": []interface{}{"$FOO:/target"},
+			"logging": types.Dict{
+				"driver": "${FOO}",
+				"options": types.Dict{
+					"user": "$USER",
+				},
+			},
+		},
+	}
+	expected := types.Dict{
+		"servicea": types.Dict{
+			"image":   "example:jenny",
+			"volumes": []interface{}{"bar:/target"},
+			"logging": types.Dict{
+				"driver": "bar",
+				"options": types.Dict{
+					"user": "jenny",
+				},
+			},
+		},
+	}
+	result, err := Interpolate(services, "service", defaultMapping)
+	assert.NoError(t, err)
+	assert.Equal(t, expected, result)
+}
+
+func TestInvalidInterpolation(t *testing.T) {
+	services := types.Dict{
+		"servicea": types.Dict{
+			"image": "${",
+		},
+	}
+	_, err := Interpolate(services, "service", defaultMapping)
+	assert.EqualError(t, err, `Invalid interpolation format for "image" option in service "servicea": "${"`)
+}

+ 8 - 0
cli/compose/loader/example1.env

@@ -0,0 +1,8 @@
+# passed through
+FOO=1
+
+# overridden in example2.env
+BAR=1
+
+# overridden in full-example.yml
+BAZ=1

+ 1 - 0
cli/compose/loader/example2.env

@@ -0,0 +1 @@
+BAR=2

+ 287 - 0
cli/compose/loader/full-example.yml

@@ -0,0 +1,287 @@
+version: "3"
+
+services:
+  foo:
+    cap_add:
+      - ALL
+
+    cap_drop:
+      - NET_ADMIN
+      - SYS_ADMIN
+
+    cgroup_parent: m-executor-abcd
+
+    # String or list
+    command: bundle exec thin -p 3000
+    # command: ["bundle", "exec", "thin", "-p", "3000"]
+
+    container_name: my-web-container
+
+    depends_on:
+      - db
+      - redis
+
+    deploy:
+      mode: replicated
+      replicas: 6
+      labels: [FOO=BAR]
+      update_config:
+        parallelism: 3
+        delay: 10s
+        failure_action: continue
+        monitor: 60s
+        max_failure_ratio: 0.3
+      resources:
+        limits:
+          cpus: '0.001'
+          memory: 50M
+        reservations:
+          cpus: '0.0001'
+          memory: 20M
+      restart_policy:
+        condition: on_failure
+        delay: 5s
+        max_attempts: 3
+        window: 120s
+      placement:
+        constraints: [node=foo]
+
+    devices:
+      - "/dev/ttyUSB0:/dev/ttyUSB0"
+
+    # String or list
+    # dns: 8.8.8.8
+    dns:
+      - 8.8.8.8
+      - 9.9.9.9
+
+    # String or list
+    # dns_search: example.com
+    dns_search:
+      - dc1.example.com
+      - dc2.example.com
+
+    domainname: foo.com
+
+    # String or list
+    # entrypoint: /code/entrypoint.sh -p 3000
+    entrypoint: ["/code/entrypoint.sh", "-p", "3000"]
+
+    # String or list
+    # env_file: .env
+    env_file:
+      - ./example1.env
+      - ./example2.env
+
+    # Mapping or list
+    # Mapping values can be strings, numbers or null
+    # Booleans are not allowed - must be quoted
+    environment:
+      RACK_ENV: development
+      SHOW: 'true'
+      SESSION_SECRET:
+      BAZ: 3
+    # environment:
+    #   - RACK_ENV=development
+    #   - SHOW=true
+    #   - SESSION_SECRET
+
+    # Items can be strings or numbers
+    expose:
+     - "3000"
+     - 8000
+
+    external_links:
+      - redis_1
+      - project_db_1:mysql
+      - project_db_1:postgresql
+
+    # Mapping or list
+    # Mapping values must be strings
+    # extra_hosts:
+    #   somehost: "162.242.195.82"
+    #   otherhost: "50.31.209.229"
+    extra_hosts:
+      - "somehost:162.242.195.82"
+      - "otherhost:50.31.209.229"
+
+    hostname: foo
+
+    healthcheck:
+      test: echo "hello world"
+      interval: 10s
+      timeout: 1s
+      retries: 5
+
+    # Any valid image reference - repo, tag, id, sha
+    image: redis
+    # image: ubuntu:14.04
+    # image: tutum/influxdb
+    # image: example-registry.com:4000/postgresql
+    # image: a4bc65fd
+    # image: busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d
+
+    ipc: host
+
+    # Mapping or list
+    # Mapping values can be strings, numbers or null
+    labels:
+      com.example.description: "Accounting webapp"
+      com.example.number: 42
+      com.example.empty-label:
+    # labels:
+    #   - "com.example.description=Accounting webapp"
+    #   - "com.example.number=42"
+    #   - "com.example.empty-label"
+
+    links:
+     - db
+     - db:database
+     - redis
+
+    logging:
+      driver: syslog
+      options:
+        syslog-address: "tcp://192.168.0.42:123"
+
+    mac_address: 02:42:ac:11:65:43
+
+    # network_mode: "bridge"
+    # network_mode: "host"
+    # network_mode: "none"
+    # Use the network mode of an arbitrary container from another service
+    # network_mode: "service:db"
+    # Use the network mode of another container, specified by name or id
+    # network_mode: "container:some-container"
+    network_mode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b"
+
+    networks:
+      some-network:
+        aliases:
+         - alias1
+         - alias3
+      other-network:
+        ipv4_address: 172.16.238.10
+        ipv6_address: 2001:3984:3989::10
+      other-other-network:
+
+    pid: "host"
+
+    ports:
+      - 3000
+      - "3000-3005"
+      - "8000:8000"
+      - "9090-9091:8080-8081"
+      - "49100:22"
+      - "127.0.0.1:8001:8001"
+      - "127.0.0.1:5000-5010:5000-5010"
+
+    privileged: true
+
+    read_only: true
+
+    restart: always
+
+    security_opt:
+      - label=level:s0:c100,c200
+      - label=type:svirt_apache_t
+
+    stdin_open: true
+
+    stop_grace_period: 20s
+
+    stop_signal: SIGUSR1
+
+    # String or list
+    # tmpfs: /run
+    tmpfs:
+      - /run
+      - /tmp
+
+    tty: true
+
+    ulimits:
+      # Single number or mapping with soft + hard limits
+      nproc: 65535
+      nofile:
+        soft: 20000
+        hard: 40000
+
+    user: someone
+
+    volumes:
+      # Just specify a path and let the Engine create a volume
+      - /var/lib/mysql
+      # Specify an absolute path mapping
+      - /opt/data:/var/lib/mysql
+      # Path on the host, relative to the Compose file
+      - .:/code
+      - ./static:/var/www/html
+      # User-relative path
+      - ~/configs:/etc/configs/:ro
+      # Named volume
+      - datavolume:/var/lib/mysql
+
+    working_dir: /code
+
+networks:
+  # Entries can be null, which specifies simply that a network
+  # called "{project name}_some-network" should be created and
+  # use the default driver
+  some-network:
+
+  other-network:
+    driver: overlay
+
+    driver_opts:
+      # Values can be strings or numbers
+      foo: "bar"
+      baz: 1
+
+    ipam:
+      driver: overlay
+      # driver_opts:
+      #   # Values can be strings or numbers
+      #   com.docker.network.enable_ipv6: "true"
+      #   com.docker.network.numeric_value: 1
+      config:
+      - subnet: 172.16.238.0/24
+        # gateway: 172.16.238.1
+      - subnet: 2001:3984:3989::/64
+        # gateway: 2001:3984:3989::1
+
+  external-network:
+    # Specifies that a pre-existing network called "external-network"
+    # can be referred to within this file as "external-network"
+    external: true
+
+  other-external-network:
+    # Specifies that a pre-existing network called "my-cool-network"
+    # can be referred to within this file as "other-external-network"
+    external:
+      name: my-cool-network
+
+volumes:
+  # Entries can be null, which specifies simply that a volume
+  # called "{project name}_some-volume" should be created and
+  # use the default driver
+  some-volume:
+
+  other-volume:
+    driver: flocker
+
+    driver_opts:
+      # Values can be strings or numbers
+      foo: "bar"
+      baz: 1
+
+  external-volume:
+    # Specifies that a pre-existing volume called "external-volume"
+    # can be referred to within this file as "external-volume"
+    external: true
+
+  other-external-volume:
+    # Specifies that a pre-existing volume called "my-cool-volume"
+    # can be referred to within this file as "other-external-volume"
+    external:
+      name: my-cool-volume

+ 9 - 3
vendor/github.com/aanand/compose-file/loader/loader.go → cli/compose/loader/loader.go

@@ -9,9 +9,9 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/aanand/compose-file/interpolation"
-	"github.com/aanand/compose-file/schema"
-	"github.com/aanand/compose-file/types"
+	"github.com/docker/docker/cli/compose/interpolation"
+	"github.com/docker/docker/cli/compose/schema"
+	"github.com/docker/docker/cli/compose/types"
 	"github.com/docker/docker/runconfig/opts"
 	units "github.com/docker/go-units"
 	shellwords "github.com/mattn/go-shellwords"
@@ -117,6 +117,8 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
 	return &cfg, nil
 }
 
+// GetUnsupportedProperties returns the list of any unsupported properties that are
+// used in the Compose files.
 func GetUnsupportedProperties(configDetails types.ConfigDetails) []string {
 	unsupported := map[string]bool{}
 
@@ -141,6 +143,8 @@ func sortedKeys(set map[string]bool) []string {
 	return keys
 }
 
+// GetDeprecatedProperties returns the list of any deprecated properties that
+// are used in the compose files.
 func GetDeprecatedProperties(configDetails types.ConfigDetails) map[string]string {
 	return getProperties(getServices(getConfigDict(configDetails)), types.DeprecatedProperties)
 }
@@ -161,6 +165,8 @@ func getProperties(services types.Dict, propertyMap map[string]string) map[strin
 	return output
 }
 
+// ForbiddenPropertiesError is returned when there are properties in the Compose
+// file that are forbidden.
 type ForbiddenPropertiesError struct {
 	Properties map[string]string
 }

+ 782 - 0
cli/compose/loader/loader_test.go

@@ -0,0 +1,782 @@
+package loader
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"sort"
+	"testing"
+	"time"
+
+	"github.com/docker/docker/cli/compose/types"
+	"github.com/stretchr/testify/assert"
+)
+
+func buildConfigDetails(source types.Dict) types.ConfigDetails {
+	workingDir, err := os.Getwd()
+	if err != nil {
+		panic(err)
+	}
+
+	return types.ConfigDetails{
+		WorkingDir: workingDir,
+		ConfigFiles: []types.ConfigFile{
+			{Filename: "filename.yml", Config: source},
+		},
+		Environment: nil,
+	}
+}
+
+var sampleYAML = `
+version: "3"
+services:
+  foo:
+    image: busybox
+    networks:
+      with_me:
+  bar:
+    image: busybox
+    environment:
+      - FOO=1
+    networks:
+      - with_ipam
+volumes:
+  hello:
+    driver: default
+    driver_opts:
+      beep: boop
+networks:
+  default:
+    driver: bridge
+    driver_opts:
+      beep: boop
+  with_ipam:
+    ipam:
+      driver: default
+      config:
+        - subnet: 172.28.0.0/16
+`
+
+var sampleDict = types.Dict{
+	"version": "3",
+	"services": types.Dict{
+		"foo": types.Dict{
+			"image":    "busybox",
+			"networks": types.Dict{"with_me": nil},
+		},
+		"bar": types.Dict{
+			"image":       "busybox",
+			"environment": []interface{}{"FOO=1"},
+			"networks":    []interface{}{"with_ipam"},
+		},
+	},
+	"volumes": types.Dict{
+		"hello": types.Dict{
+			"driver": "default",
+			"driver_opts": types.Dict{
+				"beep": "boop",
+			},
+		},
+	},
+	"networks": types.Dict{
+		"default": types.Dict{
+			"driver": "bridge",
+			"driver_opts": types.Dict{
+				"beep": "boop",
+			},
+		},
+		"with_ipam": types.Dict{
+			"ipam": types.Dict{
+				"driver": "default",
+				"config": []interface{}{
+					types.Dict{
+						"subnet": "172.28.0.0/16",
+					},
+				},
+			},
+		},
+	},
+}
+
+var sampleConfig = types.Config{
+	Services: []types.ServiceConfig{
+		{
+			Name:        "foo",
+			Image:       "busybox",
+			Environment: map[string]string{},
+			Networks: map[string]*types.ServiceNetworkConfig{
+				"with_me": nil,
+			},
+		},
+		{
+			Name:        "bar",
+			Image:       "busybox",
+			Environment: map[string]string{"FOO": "1"},
+			Networks: map[string]*types.ServiceNetworkConfig{
+				"with_ipam": nil,
+			},
+		},
+	},
+	Networks: map[string]types.NetworkConfig{
+		"default": {
+			Driver: "bridge",
+			DriverOpts: map[string]string{
+				"beep": "boop",
+			},
+		},
+		"with_ipam": {
+			Ipam: types.IPAMConfig{
+				Driver: "default",
+				Config: []*types.IPAMPool{
+					{
+						Subnet: "172.28.0.0/16",
+					},
+				},
+			},
+		},
+	},
+	Volumes: map[string]types.VolumeConfig{
+		"hello": {
+			Driver: "default",
+			DriverOpts: map[string]string{
+				"beep": "boop",
+			},
+		},
+	},
+}
+
+func TestParseYAML(t *testing.T) {
+	dict, err := ParseYAML([]byte(sampleYAML))
+	if !assert.NoError(t, err) {
+		return
+	}
+	assert.Equal(t, sampleDict, dict)
+}
+
+func TestLoad(t *testing.T) {
+	actual, err := Load(buildConfigDetails(sampleDict))
+	if !assert.NoError(t, err) {
+		return
+	}
+	assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
+	assert.Equal(t, sampleConfig.Networks, actual.Networks)
+	assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
+}
+
+func TestParseAndLoad(t *testing.T) {
+	actual, err := loadYAML(sampleYAML)
+	if !assert.NoError(t, err) {
+		return
+	}
+	assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services))
+	assert.Equal(t, sampleConfig.Networks, actual.Networks)
+	assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
+}
+
+func TestInvalidTopLevelObjectType(t *testing.T) {
+	_, err := loadYAML("1")
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
+
+	_, err = loadYAML("\"hello\"")
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
+
+	_, err = loadYAML("[\"hello\"]")
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "Top-level object must be a mapping")
+}
+
+func TestNonStringKeys(t *testing.T) {
+	_, err := loadYAML(`
+version: "3"
+123:
+  foo:
+    image: busybox
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "Non-string key at top level: 123")
+
+	_, err = loadYAML(`
+version: "3"
+services:
+  foo:
+    image: busybox
+  123:
+    image: busybox
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "Non-string key in services: 123")
+
+	_, err = loadYAML(`
+version: "3"
+services:
+  foo:
+    image: busybox
+networks:
+  default:
+    ipam:
+      config:
+        - 123: oh dear
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123")
+
+	_, err = loadYAML(`
+version: "3"
+services:
+  dict-env:
+    image: busybox
+    environment:
+      1: FOO
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1")
+}
+
+func TestSupportedVersion(t *testing.T) {
+	_, err := loadYAML(`
+version: "3"
+services:
+  foo:
+    image: busybox
+`)
+	assert.NoError(t, err)
+
+	_, err = loadYAML(`
+version: "3.0"
+services:
+  foo:
+    image: busybox
+`)
+	assert.NoError(t, err)
+}
+
+func TestUnsupportedVersion(t *testing.T) {
+	_, err := loadYAML(`
+version: "2"
+services:
+  foo:
+    image: busybox
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "version")
+
+	_, err = loadYAML(`
+version: "2.0"
+services:
+  foo:
+    image: busybox
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "version")
+}
+
+func TestInvalidVersion(t *testing.T) {
+	_, err := loadYAML(`
+version: 3
+services:
+  foo:
+    image: busybox
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "version must be a string")
+}
+
+func TestV1Unsupported(t *testing.T) {
+	_, err := loadYAML(`
+foo:
+  image: busybox
+`)
+	assert.Error(t, err)
+}
+
+func TestNonMappingObject(t *testing.T) {
+	_, err := loadYAML(`
+version: "3"
+services:
+  - foo:
+      image: busybox
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "services must be a mapping")
+
+	_, err = loadYAML(`
+version: "3"
+services:
+  foo: busybox
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "services.foo must be a mapping")
+
+	_, err = loadYAML(`
+version: "3"
+networks:
+  - default:
+      driver: bridge
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "networks must be a mapping")
+
+	_, err = loadYAML(`
+version: "3"
+networks:
+  default: bridge
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "networks.default must be a mapping")
+
+	_, err = loadYAML(`
+version: "3"
+volumes:
+  - data:
+      driver: local
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "volumes must be a mapping")
+
+	_, err = loadYAML(`
+version: "3"
+volumes:
+  data: local
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "volumes.data must be a mapping")
+}
+
+func TestNonStringImage(t *testing.T) {
+	_, err := loadYAML(`
+version: "3"
+services:
+  foo:
+    image: ["busybox", "latest"]
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "services.foo.image must be a string")
+}
+
+func TestValidEnvironment(t *testing.T) {
+	config, err := loadYAML(`
+version: "3"
+services:
+  dict-env:
+    image: busybox
+    environment:
+      FOO: "1"
+      BAR: 2
+      BAZ: 2.5
+      QUUX:
+  list-env:
+    image: busybox
+    environment:
+      - FOO=1
+      - BAR=2
+      - BAZ=2.5
+      - QUUX=
+`)
+	assert.NoError(t, err)
+
+	expected := map[string]string{
+		"FOO":  "1",
+		"BAR":  "2",
+		"BAZ":  "2.5",
+		"QUUX": "",
+	}
+
+	assert.Equal(t, 2, len(config.Services))
+
+	for _, service := range config.Services {
+		assert.Equal(t, expected, service.Environment)
+	}
+}
+
+func TestInvalidEnvironmentValue(t *testing.T) {
+	_, err := loadYAML(`
+version: "3"
+services:
+  dict-env:
+    image: busybox
+    environment:
+      FOO: ["1"]
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null")
+}
+
+func TestInvalidEnvironmentObject(t *testing.T) {
+	_, err := loadYAML(`
+version: "3"
+services:
+  dict-env:
+    image: busybox
+    environment: "FOO=1"
+`)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping")
+}
+
+func TestEnvironmentInterpolation(t *testing.T) {
+	config, err := loadYAML(`
+version: "3"
+services:
+  test:
+    image: busybox
+    labels:
+      - home1=$HOME
+      - home2=${HOME}
+      - nonexistent=$NONEXISTENT
+      - default=${NONEXISTENT-default}
+networks:
+  test:
+    driver: $HOME
+volumes:
+  test:
+    driver: $HOME
+`)
+
+	assert.NoError(t, err)
+
+	home := os.Getenv("HOME")
+
+	expectedLabels := map[string]string{
+		"home1":       home,
+		"home2":       home,
+		"nonexistent": "",
+		"default":     "default",
+	}
+
+	assert.Equal(t, expectedLabels, config.Services[0].Labels)
+	assert.Equal(t, home, config.Networks["test"].Driver)
+	assert.Equal(t, home, config.Volumes["test"].Driver)
+}
+
+func TestUnsupportedProperties(t *testing.T) {
+	dict, err := ParseYAML([]byte(`
+version: "3"
+services:
+  web:
+    image: web
+    build: ./web
+    links:
+      - bar
+  db:
+    image: db
+    build: ./db
+`))
+	assert.NoError(t, err)
+
+	configDetails := buildConfigDetails(dict)
+
+	_, err = Load(configDetails)
+	assert.NoError(t, err)
+
+	unsupported := GetUnsupportedProperties(configDetails)
+	assert.Equal(t, []string{"build", "links"}, unsupported)
+}
+
+func TestDeprecatedProperties(t *testing.T) {
+	dict, err := ParseYAML([]byte(`
+version: "3"
+services:
+  web:
+    image: web
+    container_name: web
+  db:
+    image: db
+    container_name: db
+    expose: ["5434"]
+`))
+	assert.NoError(t, err)
+
+	configDetails := buildConfigDetails(dict)
+
+	_, err = Load(configDetails)
+	assert.NoError(t, err)
+
+	deprecated := GetDeprecatedProperties(configDetails)
+	assert.Equal(t, 2, len(deprecated))
+	assert.Contains(t, deprecated, "container_name")
+	assert.Contains(t, deprecated, "expose")
+}
+
+func TestForbiddenProperties(t *testing.T) {
+	_, err := loadYAML(`
+version: "3"
+services:
+  foo:
+    image: busybox
+    volumes:
+      - /data
+    volume_driver: some-driver
+  bar:
+    extends:
+      service: foo
+`)
+
+	assert.Error(t, err)
+	assert.IsType(t, &ForbiddenPropertiesError{}, err)
+	fmt.Println(err)
+	forbidden := err.(*ForbiddenPropertiesError).Properties
+
+	assert.Equal(t, 2, len(forbidden))
+	assert.Contains(t, forbidden, "volume_driver")
+	assert.Contains(t, forbidden, "extends")
+}
+
+func durationPtr(value time.Duration) *time.Duration {
+	return &value
+}
+
+func int64Ptr(value int64) *int64 {
+	return &value
+}
+
+func uint64Ptr(value uint64) *uint64 {
+	return &value
+}
+
+func TestFullExample(t *testing.T) {
+	bytes, err := ioutil.ReadFile("full-example.yml")
+	assert.NoError(t, err)
+
+	config, err := loadYAML(string(bytes))
+	if !assert.NoError(t, err) {
+		return
+	}
+
+	workingDir, err := os.Getwd()
+	assert.NoError(t, err)
+
+	homeDir := os.Getenv("HOME")
+	stopGracePeriod := time.Duration(20 * time.Second)
+
+	expectedServiceConfig := types.ServiceConfig{
+		Name: "foo",
+
+		CapAdd:        []string{"ALL"},
+		CapDrop:       []string{"NET_ADMIN", "SYS_ADMIN"},
+		CgroupParent:  "m-executor-abcd",
+		Command:       []string{"bundle", "exec", "thin", "-p", "3000"},
+		ContainerName: "my-web-container",
+		DependsOn:     []string{"db", "redis"},
+		Deploy: types.DeployConfig{
+			Mode:     "replicated",
+			Replicas: uint64Ptr(6),
+			Labels:   map[string]string{"FOO": "BAR"},
+			UpdateConfig: &types.UpdateConfig{
+				Parallelism:     uint64Ptr(3),
+				Delay:           time.Duration(10 * time.Second),
+				FailureAction:   "continue",
+				Monitor:         time.Duration(60 * time.Second),
+				MaxFailureRatio: 0.3,
+			},
+			Resources: types.Resources{
+				Limits: &types.Resource{
+					NanoCPUs:    "0.001",
+					MemoryBytes: 50 * 1024 * 1024,
+				},
+				Reservations: &types.Resource{
+					NanoCPUs:    "0.0001",
+					MemoryBytes: 20 * 1024 * 1024,
+				},
+			},
+			RestartPolicy: &types.RestartPolicy{
+				Condition:   "on_failure",
+				Delay:       durationPtr(5 * time.Second),
+				MaxAttempts: uint64Ptr(3),
+				Window:      durationPtr(2 * time.Minute),
+			},
+			Placement: types.Placement{
+				Constraints: []string{"node=foo"},
+			},
+		},
+		Devices:    []string{"/dev/ttyUSB0:/dev/ttyUSB0"},
+		DNS:        []string{"8.8.8.8", "9.9.9.9"},
+		DNSSearch:  []string{"dc1.example.com", "dc2.example.com"},
+		DomainName: "foo.com",
+		Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"},
+		Environment: map[string]string{
+			"RACK_ENV":       "development",
+			"SHOW":           "true",
+			"SESSION_SECRET": "",
+			"FOO":            "1",
+			"BAR":            "2",
+			"BAZ":            "3",
+		},
+		Expose: []string{"3000", "8000"},
+		ExternalLinks: []string{
+			"redis_1",
+			"project_db_1:mysql",
+			"project_db_1:postgresql",
+		},
+		ExtraHosts: map[string]string{
+			"otherhost": "50.31.209.229",
+			"somehost":  "162.242.195.82",
+		},
+		HealthCheck: &types.HealthCheckConfig{
+			Test: []string{
+				"CMD-SHELL",
+				"echo \"hello world\"",
+			},
+			Interval: "10s",
+			Timeout:  "1s",
+			Retries:  uint64Ptr(5),
+		},
+		Hostname: "foo",
+		Image:    "redis",
+		Ipc:      "host",
+		Labels: map[string]string{
+			"com.example.description": "Accounting webapp",
+			"com.example.number":      "42",
+			"com.example.empty-label": "",
+		},
+		Links: []string{
+			"db",
+			"db:database",
+			"redis",
+		},
+		Logging: &types.LoggingConfig{
+			Driver: "syslog",
+			Options: map[string]string{
+				"syslog-address": "tcp://192.168.0.42:123",
+			},
+		},
+		MacAddress:  "02:42:ac:11:65:43",
+		NetworkMode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b",
+		Networks: map[string]*types.ServiceNetworkConfig{
+			"some-network": {
+				Aliases:     []string{"alias1", "alias3"},
+				Ipv4Address: "",
+				Ipv6Address: "",
+			},
+			"other-network": {
+				Ipv4Address: "172.16.238.10",
+				Ipv6Address: "2001:3984:3989::10",
+			},
+			"other-other-network": nil,
+		},
+		Pid: "host",
+		Ports: []string{
+			"3000",
+			"3000-3005",
+			"8000:8000",
+			"9090-9091:8080-8081",
+			"49100:22",
+			"127.0.0.1:8001:8001",
+			"127.0.0.1:5000-5010:5000-5010",
+		},
+		Privileged: true,
+		ReadOnly:   true,
+		Restart:    "always",
+		SecurityOpt: []string{
+			"label=level:s0:c100,c200",
+			"label=type:svirt_apache_t",
+		},
+		StdinOpen:       true,
+		StopSignal:      "SIGUSR1",
+		StopGracePeriod: &stopGracePeriod,
+		Tmpfs:           []string{"/run", "/tmp"},
+		Tty:             true,
+		Ulimits: map[string]*types.UlimitsConfig{
+			"nproc": {
+				Single: 65535,
+			},
+			"nofile": {
+				Soft: 20000,
+				Hard: 40000,
+			},
+		},
+		User: "someone",
+		Volumes: []string{
+			"/var/lib/mysql",
+			"/opt/data:/var/lib/mysql",
+			fmt.Sprintf("%s:/code", workingDir),
+			fmt.Sprintf("%s/static:/var/www/html", workingDir),
+			fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir),
+			"datavolume:/var/lib/mysql",
+		},
+		WorkingDir: "/code",
+	}
+
+	assert.Equal(t, []types.ServiceConfig{expectedServiceConfig}, config.Services)
+
+	expectedNetworkConfig := map[string]types.NetworkConfig{
+		"some-network": {},
+
+		"other-network": {
+			Driver: "overlay",
+			DriverOpts: map[string]string{
+				"foo": "bar",
+				"baz": "1",
+			},
+			Ipam: types.IPAMConfig{
+				Driver: "overlay",
+				Config: []*types.IPAMPool{
+					{Subnet: "172.16.238.0/24"},
+					{Subnet: "2001:3984:3989::/64"},
+				},
+			},
+		},
+
+		"external-network": {
+			External: types.External{
+				Name:     "external-network",
+				External: true,
+			},
+		},
+
+		"other-external-network": {
+			External: types.External{
+				Name:     "my-cool-network",
+				External: true,
+			},
+		},
+	}
+
+	assert.Equal(t, expectedNetworkConfig, config.Networks)
+
+	expectedVolumeConfig := map[string]types.VolumeConfig{
+		"some-volume": {},
+		"other-volume": {
+			Driver: "flocker",
+			DriverOpts: map[string]string{
+				"foo": "bar",
+				"baz": "1",
+			},
+		},
+		"external-volume": {
+			External: types.External{
+				Name:     "external-volume",
+				External: true,
+			},
+		},
+		"other-external-volume": {
+			External: types.External{
+				Name:     "my-cool-volume",
+				External: true,
+			},
+		},
+	}
+
+	assert.Equal(t, expectedVolumeConfig, config.Volumes)
+}
+
+func loadYAML(yaml string) (*types.Config, error) {
+	dict, err := ParseYAML([]byte(yaml))
+	if err != nil {
+		return nil, err
+	}
+
+	return Load(buildConfigDetails(dict))
+}
+
+func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
+	sort.Sort(servicesByName(services))
+	return services
+}
+
+type servicesByName []types.ServiceConfig
+
+func (sbn servicesByName) Len() int           { return len(sbn) }
+func (sbn servicesByName) Swap(i, j int)      { sbn[i], sbn[j] = sbn[j], sbn[i] }
+func (sbn servicesByName) Less(i, j int) bool { return sbn[i].Name < sbn[j].Name }

File diff suppressed because it is too large
+ 70 - 0
cli/compose/schema/bindata.go


+ 379 - 0
cli/compose/schema/data/config_schema_v3.0.json

@@ -0,0 +1,379 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "id": "config_schema_v3.0.json",
+  "type": "object",
+  "required": ["version"],
+
+  "properties": {
+    "version": {
+      "type": "string"
+    },
+
+    "services": {
+      "id": "#/properties/services",
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z0-9._-]+$": {
+          "$ref": "#/definitions/service"
+        }
+      },
+      "additionalProperties": false
+    },
+
+    "networks": {
+      "id": "#/properties/networks",
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z0-9._-]+$": {
+          "$ref": "#/definitions/network"
+        }
+      }
+    },
+
+    "volumes": {
+      "id": "#/properties/volumes",
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z0-9._-]+$": {
+          "$ref": "#/definitions/volume"
+        }
+      },
+      "additionalProperties": false
+    }
+  },
+
+  "additionalProperties": false,
+
+  "definitions": {
+
+    "service": {
+      "id": "#/definitions/service",
+      "type": "object",
+
+      "properties": {
+        "deploy": {"$ref": "#/definitions/deployment"},
+        "build": {
+          "oneOf": [
+            {"type": "string"},
+            {
+              "type": "object",
+              "properties": {
+                "context": {"type": "string"},
+                "dockerfile": {"type": "string"},
+                "args": {"$ref": "#/definitions/list_or_dict"}
+              },
+              "additionalProperties": false
+            }
+          ]
+        },
+        "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "cgroup_parent": {"type": "string"},
+        "command": {
+          "oneOf": [
+            {"type": "string"},
+            {"type": "array", "items": {"type": "string"}}
+          ]
+        },
+        "container_name": {"type": "string"},
+        "depends_on": {"$ref": "#/definitions/list_of_strings"},
+        "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "dns": {"$ref": "#/definitions/string_or_list"},
+        "dns_search": {"$ref": "#/definitions/string_or_list"},
+        "domainname": {"type": "string"},
+        "entrypoint": {
+          "oneOf": [
+            {"type": "string"},
+            {"type": "array", "items": {"type": "string"}}
+          ]
+        },
+        "env_file": {"$ref": "#/definitions/string_or_list"},
+        "environment": {"$ref": "#/definitions/list_or_dict"},
+
+        "expose": {
+          "type": "array",
+          "items": {
+            "type": ["string", "number"],
+            "format": "expose"
+          },
+          "uniqueItems": true
+        },
+
+        "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
+        "healthcheck": {"$ref": "#/definitions/healthcheck"},
+        "hostname": {"type": "string"},
+        "image": {"type": "string"},
+        "ipc": {"type": "string"},
+        "labels": {"$ref": "#/definitions/list_or_dict"},
+        "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+
+        "logging": {
+            "type": "object",
+
+            "properties": {
+                "driver": {"type": "string"},
+                "options": {
+                  "type": "object",
+                  "patternProperties": {
+                    "^.+$": {"type": ["string", "number", "null"]}
+                  }
+                }
+            },
+            "additionalProperties": false
+        },
+
+        "mac_address": {"type": "string"},
+        "network_mode": {"type": "string"},
+
+        "networks": {
+          "oneOf": [
+            {"$ref": "#/definitions/list_of_strings"},
+            {
+              "type": "object",
+              "patternProperties": {
+                "^[a-zA-Z0-9._-]+$": {
+                  "oneOf": [
+                    {
+                      "type": "object",
+                      "properties": {
+                        "aliases": {"$ref": "#/definitions/list_of_strings"},
+                        "ipv4_address": {"type": "string"},
+                        "ipv6_address": {"type": "string"}
+                      },
+                      "additionalProperties": false
+                    },
+                    {"type": "null"}
+                  ]
+                }
+              },
+              "additionalProperties": false
+            }
+          ]
+        },
+        "pid": {"type": ["string", "null"]},
+
+        "ports": {
+          "type": "array",
+          "items": {
+            "type": ["string", "number"],
+            "format": "ports"
+          },
+          "uniqueItems": true
+        },
+
+        "privileged": {"type": "boolean"},
+        "read_only": {"type": "boolean"},
+        "restart": {"type": "string"},
+        "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "shm_size": {"type": ["number", "string"]},
+        "stdin_open": {"type": "boolean"},
+        "stop_signal": {"type": "string"},
+        "stop_grace_period": {"type": "string", "format": "duration"},
+        "tmpfs": {"$ref": "#/definitions/string_or_list"},
+        "tty": {"type": "boolean"},
+        "ulimits": {
+          "type": "object",
+          "patternProperties": {
+            "^[a-z]+$": {
+              "oneOf": [
+                {"type": "integer"},
+                {
+                  "type":"object",
+                  "properties": {
+                    "hard": {"type": "integer"},
+                    "soft": {"type": "integer"}
+                  },
+                  "required": ["soft", "hard"],
+                  "additionalProperties": false
+                }
+              ]
+            }
+          }
+        },
+        "user": {"type": "string"},
+        "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
+        "working_dir": {"type": "string"}
+      },
+      "additionalProperties": false
+    },
+
+    "healthcheck": {
+      "id": "#/definitions/healthcheck",
+      "type": ["object", "null"],
+      "properties": {
+        "interval": {"type":"string"},
+        "timeout": {"type":"string"},
+        "retries": {"type": "number"},
+        "test": {
+          "oneOf": [
+            {"type": "string"},
+            {"type": "array", "items": {"type": "string"}}
+          ]
+        },
+        "disable": {"type": "boolean"}
+      },
+      "additionalProperties": false
+    },
+    "deployment": {
+      "id": "#/definitions/deployment",
+      "type": ["object", "null"],
+      "properties": {
+        "mode": {"type": "string"},
+        "replicas": {"type": "integer"},
+        "labels": {"$ref": "#/definitions/list_or_dict"},
+        "update_config": {
+          "type": "object",
+          "properties": {
+            "parallelism": {"type": "integer"},
+            "delay": {"type": "string", "format": "duration"},
+            "failure_action": {"type": "string"},
+            "monitor": {"type": "string", "format": "duration"},
+            "max_failure_ratio": {"type": "number"}
+          },
+          "additionalProperties": false
+        },
+        "resources": {
+          "type": "object",
+          "properties": {
+            "limits": {"$ref": "#/definitions/resource"},
+            "reservations": {"$ref": "#/definitions/resource"}
+          }
+        },
+        "restart_policy": {
+          "type": "object",
+          "properties": {
+            "condition": {"type": "string"},
+            "delay": {"type": "string", "format": "duration"},
+            "max_attempts": {"type": "integer"},
+            "window": {"type": "string", "format": "duration"}
+          },
+          "additionalProperties": false
+        },
+        "placement": {
+          "type": "object",
+          "properties": {
+            "constraints": {"type": "array", "items": {"type": "string"}}
+          },
+          "additionalProperties": false
+        }
+      },
+      "additionalProperties": false
+    },
+
+    "resource": {
+      "id": "#/definitions/resource",
+      "type": "object",
+      "properties": {
+        "cpus": {"type": "string"},
+        "memory": {"type": "string"}
+      },
+      "additionalProperties": false
+    },
+
+    "network": {
+      "id": "#/definitions/network",
+      "type": ["object", "null"],
+      "properties": {
+        "driver": {"type": "string"},
+        "driver_opts": {
+          "type": "object",
+          "patternProperties": {
+            "^.+$": {"type": ["string", "number"]}
+          }
+        },
+        "ipam": {
+          "type": "object",
+          "properties": {
+            "driver": {"type": "string"},
+            "config": {
+              "type": "array",
+              "items": {
+                "type": "object",
+                "properties": {
+                  "subnet": {"type": "string"}
+                },
+                "additionalProperties": false
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        "external": {
+          "type": ["boolean", "object"],
+          "properties": {
+            "name": {"type": "string"}
+          },
+          "additionalProperties": false
+        },
+        "labels": {"$ref": "#/definitions/list_or_dict"}
+      },
+      "additionalProperties": false
+    },
+
+    "volume": {
+      "id": "#/definitions/volume",
+      "type": ["object", "null"],
+      "properties": {
+        "driver": {"type": "string"},
+        "driver_opts": {
+          "type": "object",
+          "patternProperties": {
+            "^.+$": {"type": ["string", "number"]}
+          }
+        },
+        "external": {
+          "type": ["boolean", "object"],
+          "properties": {
+            "name": {"type": "string"}
+          }
+        }
+      },
+      "labels": {"$ref": "#/definitions/list_or_dict"},
+      "additionalProperties": false
+    },
+
+    "string_or_list": {
+      "oneOf": [
+        {"type": "string"},
+        {"$ref": "#/definitions/list_of_strings"}
+      ]
+    },
+
+    "list_of_strings": {
+      "type": "array",
+      "items": {"type": "string"},
+      "uniqueItems": true
+    },
+
+    "list_or_dict": {
+      "oneOf": [
+        {
+          "type": "object",
+          "patternProperties": {
+            ".+": {
+              "type": ["string", "number", "null"]
+            }
+          },
+          "additionalProperties": false
+        },
+        {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
+      ]
+    },
+
+    "constraints": {
+      "service": {
+        "id": "#/definitions/constraints/service",
+        "anyOf": [
+          {"required": ["build"]},
+          {"required": ["image"]}
+        ],
+        "properties": {
+          "build": {
+            "required": ["context"]
+          }
+        }
+      }
+    }
+  }
+}

+ 1 - 1
vendor/github.com/aanand/compose-file/schema/schema.go → cli/compose/schema/schema.go

@@ -1,6 +1,6 @@
 package schema
 
-//go:generate go-bindata -pkg schema data
+//go:generate go-bindata -pkg schema -nometadata data
 
 import (
 	"fmt"

+ 35 - 0
cli/compose/schema/schema_test.go

@@ -0,0 +1,35 @@
+package schema
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type dict map[string]interface{}
+
+func TestValid(t *testing.T) {
+	config := dict{
+		"version": "2.1",
+		"services": dict{
+			"foo": dict{
+				"image": "busybox",
+			},
+		},
+	}
+
+	assert.NoError(t, Validate(config))
+}
+
+func TestUndefinedTopLevelOption(t *testing.T) {
+	config := dict{
+		"version": "2.1",
+		"helicopters": dict{
+			"foo": dict{
+				"image": "busybox",
+			},
+		},
+	}
+
+	assert.Error(t, Validate(config))
+}

+ 7 - 15
vendor/github.com/aanand/compose-file/template/template.go → cli/compose/template/template.go

@@ -16,6 +16,8 @@ var patternString = fmt.Sprintf(
 
 var pattern = regexp.MustCompile(patternString)
 
+// InvalidTemplateError is returned when a variable template is not in a valid
+// format
 type InvalidTemplateError struct {
 	Template string
 }
@@ -24,23 +26,14 @@ func (e InvalidTemplateError) Error() string {
 	return fmt.Sprintf("Invalid template: %#v", e.Template)
 }
 
-// A user-supplied function which maps from variable names to values.
+// Mapping is a user-supplied function which maps from variable names to values.
 // Returns the value as a string and a bool indicating whether
 // the value is present, to distinguish between an empty string
 // and the absence of a value.
 type Mapping func(string) (string, bool)
 
+// Substitute variables in the string with their values
 func Substitute(template string, mapping Mapping) (result string, err *InvalidTemplateError) {
-	defer func() {
-		if r := recover(); r != nil {
-			if e, ok := r.(*InvalidTemplateError); ok {
-				err = e
-			} else {
-				panic(r)
-			}
-		}
-	}()
-
 	result = pattern.ReplaceAllStringFunc(template, func(substring string) string {
 		matches := pattern.FindStringSubmatch(substring)
 		groups := make(map[string]string)
@@ -87,11 +80,11 @@ func Substitute(template string, mapping Mapping) (result string, err *InvalidTe
 			return escaped
 		}
 
-		panic(&InvalidTemplateError{Template: template})
+		err = &InvalidTemplateError{Template: template}
 		return ""
 	})
 
-	return
+	return result, err
 }
 
 // Split the string at the first occurrence of sep, and return the part before the separator,
@@ -102,7 +95,6 @@ func partition(s, sep string) (string, string) {
 	if strings.Contains(s, sep) {
 		parts := strings.SplitN(s, sep, 2)
 		return parts[0], parts[1]
-	} else {
-		return s, ""
 	}
+	return s, ""
 }

+ 83 - 0
cli/compose/template/template_test.go

@@ -0,0 +1,83 @@
+package template
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+var defaults = map[string]string{
+	"FOO": "first",
+	"BAR": "",
+}
+
+func defaultMapping(name string) (string, bool) {
+	val, ok := defaults[name]
+	return val, ok
+}
+
+func TestEscaped(t *testing.T) {
+	result, err := Substitute("$${foo}", defaultMapping)
+	assert.NoError(t, err)
+	assert.Equal(t, "${foo}", result)
+}
+
+func TestInvalid(t *testing.T) {
+	invalidTemplates := []string{
+		"${",
+		"$}",
+		"${}",
+		"${ }",
+		"${ foo}",
+		"${foo }",
+		"${foo!}",
+	}
+
+	for _, template := range invalidTemplates {
+		_, err := Substitute(template, defaultMapping)
+		assert.Error(t, err)
+		assert.IsType(t, &InvalidTemplateError{}, err)
+	}
+}
+
+func TestNoValueNoDefault(t *testing.T) {
+	for _, template := range []string{"This ${missing} var", "This ${BAR} var"} {
+		result, err := Substitute(template, defaultMapping)
+		assert.NoError(t, err)
+		assert.Equal(t, "This  var", result)
+	}
+}
+
+func TestValueNoDefault(t *testing.T) {
+	for _, template := range []string{"This $FOO var", "This ${FOO} var"} {
+		result, err := Substitute(template, defaultMapping)
+		assert.NoError(t, err)
+		assert.Equal(t, "This first var", result)
+	}
+}
+
+func TestNoValueWithDefault(t *testing.T) {
+	for _, template := range []string{"ok ${missing:-def}", "ok ${missing-def}"} {
+		result, err := Substitute(template, defaultMapping)
+		assert.NoError(t, err)
+		assert.Equal(t, "ok def", result)
+	}
+}
+
+func TestEmptyValueWithSoftDefault(t *testing.T) {
+	result, err := Substitute("ok ${BAR:-def}", defaultMapping)
+	assert.NoError(t, err)
+	assert.Equal(t, "ok def", result)
+}
+
+func TestEmptyValueWithHardDefault(t *testing.T) {
+	result, err := Substitute("ok ${BAR-def}", defaultMapping)
+	assert.NoError(t, err)
+	assert.Equal(t, "ok ", result)
+}
+
+func TestNonAlphanumericDefault(t *testing.T) {
+	result, err := Substitute("ok ${BAR:-/non:-alphanumeric}", defaultMapping)
+	assert.NoError(t, err)
+	assert.Equal(t, "ok /non:-alphanumeric", result)
+}

+ 27 - 2
vendor/github.com/aanand/compose-file/types/types.go → cli/compose/types/types.go

@@ -4,6 +4,7 @@ import (
 	"time"
 )
 
+// UnsupportedProperties not yet supported by this implementation of the compose file
 var UnsupportedProperties = []string{
 	"build",
 	"cap_add",
@@ -27,11 +28,15 @@ var UnsupportedProperties = []string{
 	"tmpfs",
 }
 
+// DeprecatedProperties that were removed from the v3 format, but their
+// use should not impact the behaviour of the application.
 var DeprecatedProperties = map[string]string{
 	"container_name": "Setting the container name is not supported.",
 	"expose":         "Exposing ports is unnecessary - services on the same network can access each other's containers on any port.",
 }
 
+// ForbiddenProperties that are not supported in this implementation of the
+// compose file.
 var ForbiddenProperties = map[string]string{
 	"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.",
 	"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.",
@@ -43,25 +48,30 @@ var ForbiddenProperties = map[string]string{
 	"memswap_limit": "Set resource limits using deploy.resources",
 }
 
+// Dict is a mapping of strings to interface{}
 type Dict map[string]interface{}
 
+// ConfigFile is a filename and the contents of the file as a Dict
 type ConfigFile struct {
 	Filename string
 	Config   Dict
 }
 
+// ConfigDetails are the details about a group of ConfigFiles
 type ConfigDetails struct {
 	WorkingDir  string
 	ConfigFiles []ConfigFile
 	Environment map[string]string
 }
 
+// Config is a full compose file configuration
 type Config struct {
 	Services []ServiceConfig
 	Networks map[string]NetworkConfig
 	Volumes  map[string]VolumeConfig
 }
 
+// ServiceConfig is the configuration of one service
 type ServiceConfig struct {
 	Name string
 
@@ -73,8 +83,8 @@ type ServiceConfig struct {
 	DependsOn       []string `mapstructure:"depends_on"`
 	Deploy          DeployConfig
 	Devices         []string
-	Dns             []string          `compose:"string_or_list"`
-	DnsSearch       []string          `mapstructure:"dns_search" compose:"string_or_list"`
+	DNS             []string          `compose:"string_or_list"`
+	DNSSearch       []string          `mapstructure:"dns_search" compose:"string_or_list"`
 	DomainName      string            `mapstructure:"domainname"`
 	Entrypoint      []string          `compose:"shell_command"`
 	Environment     map[string]string `compose:"list_or_dict_equals"`
@@ -108,11 +118,13 @@ type ServiceConfig struct {
 	WorkingDir      string `mapstructure:"working_dir"`
 }
 
+// LoggingConfig the logging configuration for a service
 type LoggingConfig struct {
 	Driver  string
 	Options map[string]string
 }
 
+// DeployConfig the deployment configuration for a service
 type DeployConfig struct {
 	Mode          string
 	Replicas      *uint64
@@ -123,6 +135,7 @@ type DeployConfig struct {
 	Placement     Placement
 }
 
+// HealthCheckConfig the healthcheck configuration for a service
 type HealthCheckConfig struct {
 	Test     []string `compose:"healthcheck"`
 	Timeout  string
@@ -131,6 +144,7 @@ type HealthCheckConfig struct {
 	Disable  bool
 }
 
+// UpdateConfig the service update configuration
 type UpdateConfig struct {
 	Parallelism     *uint64
 	Delay           time.Duration
@@ -139,19 +153,23 @@ type UpdateConfig struct {
 	MaxFailureRatio float32 `mapstructure:"max_failure_ratio"`
 }
 
+// Resources the resource limits and reservations
 type Resources struct {
 	Limits       *Resource
 	Reservations *Resource
 }
 
+// Resource is a resource to be limited or reserved
 type Resource struct {
 	// TODO: types to convert from units and ratios
 	NanoCPUs    string    `mapstructure:"cpus"`
 	MemoryBytes UnitBytes `mapstructure:"memory"`
 }
 
+// UnitBytes is the bytes type
 type UnitBytes int64
 
+// RestartPolicy the service restart policy
 type RestartPolicy struct {
 	Condition   string
 	Delay       *time.Duration
@@ -159,22 +177,26 @@ type RestartPolicy struct {
 	Window      *time.Duration
 }
 
+// Placement constraints for the service
 type Placement struct {
 	Constraints []string
 }
 
+// ServiceNetworkConfig is the network configuration for a service
 type ServiceNetworkConfig struct {
 	Aliases     []string
 	Ipv4Address string `mapstructure:"ipv4_address"`
 	Ipv6Address string `mapstructure:"ipv6_address"`
 }
 
+// UlimitsConfig the ulimit configuration
 type UlimitsConfig struct {
 	Single int
 	Soft   int
 	Hard   int
 }
 
+// NetworkConfig for a network
 type NetworkConfig struct {
 	Driver     string
 	DriverOpts map[string]string `mapstructure:"driver_opts"`
@@ -183,15 +205,18 @@ type NetworkConfig struct {
 	Labels     map[string]string `compose:"list_or_dict_equals"`
 }
 
+// IPAMConfig for a network
 type IPAMConfig struct {
 	Driver string
 	Config []*IPAMPool
 }
 
+// IPAMPool for a network
 type IPAMPool struct {
 	Subnet string
 }
 
+// VolumeConfig for a volume
 type VolumeConfig struct {
 	Driver     string
 	DriverOpts map[string]string `mapstructure:"driver_opts"`

+ 1 - 0
hack/dockerfile/binaries-commits

@@ -6,3 +6,4 @@ CONTAINERD_COMMIT=03e5862ec0d8d3b3f750e19fca3ee367e13c090e
 TINI_COMMIT=949e6facb77383876aeff8a6944dde66b3089574
 LIBNETWORK_COMMIT=0f534354b813003a754606689722fe253101bc4e
 VNDR_COMMIT=f56bd4504b4fad07a357913687fb652ee54bb3b0
+BINDATA_COMMIT=a0ff2567cfb70903282db057e799fd826784d41d

+ 12 - 0
hack/dockerfile/install-binaries.sh

@@ -46,6 +46,14 @@ install_proxy() {
 	go build -ldflags="$PROXY_LDFLAGS" -o /usr/local/bin/docker-proxy github.com/docker/libnetwork/cmd/proxy
 }
 
+install_bindata() {
+    echo "Install go-bindata version $BINDATA_COMMIT"
+    git clone https://github.com/jteeuwen/go-bindata "$GOPATH/src/github.com/jteeuwen/go-bindata"
+    cd $GOPATH/src/github.com/jteeuwen/go-bindata
+    git checkout -q "$BINDATA_COMMIT"
+	go build -o /usr/local/bin/go-bindata github.com/jteeuwen/go-bindata/go-bindata
+}
+
 for prog in "$@"
 do
 	case $prog in
@@ -99,6 +107,10 @@ do
 			go build -v -o /usr/local/bin/vndr .
 			;;
 
+        bindata)
+            install_bindata
+            ;;
+
 		*)
 			echo echo "Usage: $0 [tomlv|runc|containerd|tini|proxy]"
 			exit 1

+ 1 - 1
hack/validate/lint

@@ -4,7 +4,7 @@ export SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 source "${SCRIPTDIR}/.validate"
 
 IFS=$'\n'
-files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/' | grep -v '^api/types/container/' || true) )
+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) )
 unset IFS
 
 errors=()

+ 0 - 1
vendor.conf

@@ -132,7 +132,6 @@ github.com/flynn-archive/go-shlex 3f9db97f856818214da2e1057f8ad84803971cff
 github.com/docker/go-metrics 86138d05f285fd9737a99bee2d9be30866b59d72
 
 # composefile
-github.com/aanand/compose-file a3e58764f50597b6217fec07e9bff7225c4a1719
 github.com/mitchellh/mapstructure f3009df150dadf309fdee4a54ed65c124afad715
 github.com/xeipuuv/gojsonpointer e0fe6f68307607d540ed8eac07a342c33fa1b54a
 github.com/xeipuuv/gojsonreference e02fc20de94c78484cd5ffb007f8af96be030a45

+ 0 - 191
vendor/github.com/aanand/compose-file/LICENSE

@@ -1,191 +0,0 @@
-
-                                 Apache License
-                           Version 2.0, January 2004
-                        https://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   Copyright 2016 Docker, Inc.
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       https://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.

File diff suppressed because it is too large
+ 0 - 70
vendor/github.com/aanand/compose-file/schema/bindata.go


Some files were not shown because too many files changed in this diff