Merge pull request #30597 from dnephin/add-expanded-mount-format-to-stack-deploy

Add expanded mount format to stack deploy
This commit is contained in:
Justin Cormack 2017-03-14 17:53:28 +00:00 committed by GitHub
commit 49376cddab
11 changed files with 535 additions and 179 deletions

View file

@ -1,21 +1,19 @@
package convert
import (
"fmt"
"strings"
"github.com/docker/docker/api/types/mount"
composetypes "github.com/docker/docker/cli/compose/types"
"github.com/pkg/errors"
)
type volumes map[string]composetypes.VolumeConfig
// Volumes from compose-file types to engine api types
func Volumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) {
var mounts []mount.Mount
for _, volumeSpec := range serviceVolumes {
mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace)
for _, volumeConfig := range serviceVolumes {
mount, err := convertVolumeToMount(volumeConfig, stackVolumes, namespace)
if err != nil {
return nil, err
}
@ -24,108 +22,65 @@ func Volumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace)
return mounts, nil
}
func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Namespace) (mount.Mount, error) {
var source, target string
var mode []string
func convertVolumeToMount(
volume composetypes.ServiceVolumeConfig,
stackVolumes volumes,
namespace Namespace,
) (mount.Mount, error) {
result := mount.Mount{
Type: mount.Type(volume.Type),
Source: volume.Source,
Target: volume.Target,
ReadOnly: volume.ReadOnly,
}
// TODO: split Windows path mappings properly
parts := strings.SplitN(volumeSpec, ":", 3)
// Anonymous volumes
if volume.Source == "" {
return result, nil
}
if volume.Type == "volume" && volume.Bind != nil {
return result, errors.New("bind options are incompatible with type volume")
}
if volume.Type == "bind" && volume.Volume != nil {
return result, errors.New("volume options are incompatible with type bind")
}
for _, part := range parts {
if strings.TrimSpace(part) == "" {
return mount.Mount{}, fmt.Errorf("invalid volume: %s", volumeSpec)
if volume.Bind != nil {
result.BindOptions = &mount.BindOptions{
Propagation: mount.Propagation(volume.Bind.Propagation),
}
}
switch len(parts) {
case 3:
source = parts[0]
target = parts[1]
mode = strings.Split(parts[2], ",")
case 2:
source = parts[0]
target = parts[1]
case 1:
target = parts[0]
// Binds volumes
if volume.Type == "bind" {
return result, nil
}
if source == "" {
// Anonymous volume
return mount.Mount{
Type: mount.TypeVolume,
Target: target,
}, nil
}
// TODO: catch Windows paths here
if strings.HasPrefix(source, "/") {
return mount.Mount{
Type: mount.TypeBind,
Source: source,
Target: target,
ReadOnly: isReadOnly(mode),
BindOptions: getBindOptions(mode),
}, nil
}
stackVolume, exists := stackVolumes[source]
stackVolume, exists := stackVolumes[volume.Source]
if !exists {
return mount.Mount{}, fmt.Errorf("undefined volume: %s", source)
return result, errors.Errorf("undefined volume: %s", volume.Source)
}
var volumeOptions *mount.VolumeOptions
if stackVolume.External.Name != "" {
volumeOptions = &mount.VolumeOptions{
NoCopy: isNoCopy(mode),
}
source = stackVolume.External.Name
} else {
volumeOptions = &mount.VolumeOptions{
Labels: AddStackLabel(namespace, stackVolume.Labels),
NoCopy: isNoCopy(mode),
}
result.Source = namespace.Scope(volume.Source)
result.VolumeOptions = &mount.VolumeOptions{}
if stackVolume.Driver != "" {
volumeOptions.DriverConfig = &mount.Driver{
Name: stackVolume.Driver,
Options: stackVolume.DriverOpts,
}
}
source = namespace.Scope(source)
if volume.Volume != nil {
result.VolumeOptions.NoCopy = volume.Volume.NoCopy
}
return mount.Mount{
Type: mount.TypeVolume,
Source: source,
Target: target,
ReadOnly: isReadOnly(mode),
VolumeOptions: volumeOptions,
}, nil
}
func modeHas(mode []string, field string) bool {
for _, item := range mode {
if item == field {
return true
// External named volumes
if stackVolume.External.External {
result.Source = stackVolume.External.Name
return result, nil
}
result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels)
if stackVolume.Driver != "" || stackVolume.DriverOpts != nil {
result.VolumeOptions.DriverConfig = &mount.Driver{
Name: stackVolume.Driver,
Options: stackVolume.DriverOpts,
}
}
return false
}
func isReadOnly(mode []string) bool {
return modeHas(mode, "ro")
}
func isNoCopy(mode []string) bool {
return modeHas(mode, "nocopy")
}
func getBindOptions(mode []string) *mount.BindOptions {
for _, item := range mode {
for _, propagation := range mount.Propagations {
if mount.Propagation(item) == propagation {
return &mount.BindOptions{Propagation: mount.Propagation(item)}
}
}
}
return nil
// Named volumes
return result, nil
}

View file

@ -8,51 +8,48 @@ import (
"github.com/docker/docker/pkg/testutil/assert"
)
func TestIsReadOnly(t *testing.T) {
assert.Equal(t, isReadOnly([]string{"foo", "bar", "ro"}), true)
assert.Equal(t, isReadOnly([]string{"ro"}), true)
assert.Equal(t, isReadOnly([]string{}), false)
assert.Equal(t, isReadOnly([]string{"foo", "rw"}), false)
assert.Equal(t, isReadOnly([]string{"foo"}), false)
}
func TestIsNoCopy(t *testing.T) {
assert.Equal(t, isNoCopy([]string{"foo", "bar", "nocopy"}), true)
assert.Equal(t, isNoCopy([]string{"nocopy"}), true)
assert.Equal(t, isNoCopy([]string{}), false)
assert.Equal(t, isNoCopy([]string{"foo", "rw"}), false)
}
func TestGetBindOptions(t *testing.T) {
opts := getBindOptions([]string{"slave"})
expected := mount.BindOptions{Propagation: mount.PropagationSlave}
assert.Equal(t, *opts, expected)
}
func TestGetBindOptionsNone(t *testing.T) {
opts := getBindOptions([]string{"ro"})
assert.Equal(t, opts, (*mount.BindOptions)(nil))
}
func TestConvertVolumeToMountAnonymousVolume(t *testing.T) {
stackVolumes := volumes{}
namespace := NewNamespace("foo")
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Target: "/foo/bar",
}
expected := mount.Mount{
Type: mount.TypeVolume,
Target: "/foo/bar",
}
mount, err := convertVolumeToMount("/foo/bar", stackVolumes, namespace)
mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo"))
assert.NilError(t, err)
assert.DeepEqual(t, mount, expected)
}
func TestConvertVolumeToMountInvalidFormat(t *testing.T) {
func TestConvertVolumeToMountConflictingOptionsBind(t *testing.T) {
namespace := NewNamespace("foo")
invalids := []string{"::", "::cc", ":bb:", "aa::", "aa::cc", "aa:bb:", " : : ", " : :cc", " :bb: ", "aa: : ", "aa: :cc", "aa:bb: "}
for _, vol := range invalids {
_, err := convertVolumeToMount(vol, volumes{}, namespace)
assert.Error(t, err, "invalid volume: "+vol)
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Source: "foo",
Target: "/target",
Bind: &composetypes.ServiceVolumeBind{
Propagation: "slave",
},
}
_, err := convertVolumeToMount(config, volumes{}, namespace)
assert.Error(t, err, "bind options are incompatible")
}
func TestConvertVolumeToMountConflictingOptionsVolume(t *testing.T) {
namespace := NewNamespace("foo")
config := composetypes.ServiceVolumeConfig{
Type: "bind",
Source: "/foo",
Target: "/target",
Volume: &composetypes.ServiceVolumeVolume{
NoCopy: true,
},
}
_, err := convertVolumeToMount(config, volumes{}, namespace)
assert.Error(t, err, "volume options are incompatible")
}
func TestConvertVolumeToMountNamedVolume(t *testing.T) {
@ -84,9 +81,19 @@ func TestConvertVolumeToMountNamedVolume(t *testing.T) {
"opt": "value",
},
},
NoCopy: true,
},
}
mount, err := convertVolumeToMount("normal:/foo:ro", stackVolumes, namespace)
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Source: "normal",
Target: "/foo",
ReadOnly: true,
Volume: &composetypes.ServiceVolumeVolume{
NoCopy: true,
},
}
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
assert.NilError(t, err)
assert.DeepEqual(t, mount, expected)
}
@ -109,7 +116,12 @@ func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) {
NoCopy: false,
},
}
mount, err := convertVolumeToMount("outside:/foo", stackVolumes, namespace)
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Source: "outside",
Target: "/foo",
}
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
assert.NilError(t, err)
assert.DeepEqual(t, mount, expected)
}
@ -132,7 +144,15 @@ func TestConvertVolumeToMountNamedVolumeExternalNoCopy(t *testing.T) {
NoCopy: true,
},
}
mount, err := convertVolumeToMount("outside:/foo:nocopy", stackVolumes, namespace)
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Source: "outside",
Target: "/foo",
Volume: &composetypes.ServiceVolumeVolume{
NoCopy: true,
},
}
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
assert.NilError(t, err)
assert.DeepEqual(t, mount, expected)
}
@ -147,13 +167,26 @@ func TestConvertVolumeToMountBind(t *testing.T) {
ReadOnly: true,
BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared},
}
mount, err := convertVolumeToMount("/bar:/foo:ro,shared", stackVolumes, namespace)
config := composetypes.ServiceVolumeConfig{
Type: "bind",
Source: "/bar",
Target: "/foo",
ReadOnly: true,
Bind: &composetypes.ServiceVolumeBind{Propagation: "shared"},
}
mount, err := convertVolumeToMount(config, stackVolumes, namespace)
assert.NilError(t, err)
assert.DeepEqual(t, mount, expected)
}
func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) {
namespace := NewNamespace("foo")
_, err := convertVolumeToMount("unknown:/foo:ro", volumes{}, namespace)
config := composetypes.ServiceVolumeConfig{
Type: "volume",
Source: "unknown",
Target: "/foo",
ReadOnly: true,
}
_, err := convertVolumeToMount(config, volumes{}, namespace)
assert.Error(t, err, "undefined volume: unknown")
}

View file

@ -251,6 +251,8 @@ func transformHook(
return transformMappingOrList(data, "="), nil
case reflect.TypeOf(types.MappingWithColon{}):
return transformMappingOrList(data, ":"), nil
case reflect.TypeOf(types.ServiceVolumeConfig{}):
return transformServiceVolumeConfig(data)
}
return data, nil
}
@ -333,10 +335,7 @@ func LoadService(name string, serviceDict types.Dict, workingDir string) (*types
return nil, err
}
if err := resolveVolumePaths(serviceConfig.Volumes, workingDir); err != nil {
return nil, err
}
resolveVolumePaths(serviceConfig.Volumes, workingDir)
return serviceConfig, nil
}
@ -369,22 +368,15 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string) e
return nil
}
func resolveVolumePaths(volumes []string, workingDir string) error {
for i, mapping := range volumes {
parts := strings.SplitN(mapping, ":", 2)
if len(parts) == 1 {
func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string) {
for i, volume := range volumes {
if volume.Type != "bind" {
continue
}
if strings.HasPrefix(parts[0], ".") {
parts[0] = absPath(workingDir, parts[0])
}
parts[0] = expandUser(parts[0])
volumes[i] = strings.Join(parts, ":")
volume.Source = absPath(workingDir, expandUser(volume.Source))
volumes[i] = volume
}
return nil
}
// TODO: make this more robust
@ -555,6 +547,20 @@ func transformServiceSecret(data interface{}) (interface{}, error) {
}
}
func transformServiceVolumeConfig(data interface{}) (interface{}, error) {
switch value := data.(type) {
case string:
return parseVolume(value)
case types.Dict:
return data, nil
case map[string]interface{}:
return data, nil
default:
return data, fmt.Errorf("invalid type %T for service volume", value)
}
}
func transformServiceNetworkMap(value interface{}) (interface{}, error) {
if list, ok := value.([]interface{}); ok {
mapValue := map[interface{}]interface{}{}

View file

@ -881,13 +881,13 @@ func TestFullExample(t *testing.T) {
},
},
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",
Volumes: []types.ServiceVolumeConfig{
{Target: "/var/lib/mysql", Type: "volume"},
{Source: "/opt/data", Target: "/var/lib/mysql", Type: "bind"},
{Source: workingDir, Target: "/code", Type: "bind"},
{Source: workingDir + "/static", Target: "/var/www/html", Type: "bind"},
{Source: homeDir + "/configs", Target: "/etc/configs/", Type: "bind", ReadOnly: true},
{Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"},
},
WorkingDir: "/code",
}
@ -1085,3 +1085,31 @@ services:
assert.Equal(t, 1, len(config.Services))
assert.Equal(t, expected, config.Services[0].Ports)
}
func TestLoadExpandedMountFormat(t *testing.T) {
config, err := loadYAML(`
version: "3.1"
services:
web:
image: busybox
volumes:
- type: volume
source: foo
target: /target
read_only: true
volumes:
foo: {}
`)
assert.NoError(t, err)
expected := types.ServiceVolumeConfig{
Type: "volume",
Source: "foo",
Target: "/target",
ReadOnly: true,
}
assert.Equal(t, 1, len(config.Services))
assert.Equal(t, 1, len(config.Services[0].Volumes))
assert.Equal(t, expected, config.Services[0].Volumes[0])
}

View file

@ -0,0 +1,119 @@
package loader
import (
"strings"
"unicode"
"unicode/utf8"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/cli/compose/types"
"github.com/pkg/errors"
)
func parseVolume(spec string) (types.ServiceVolumeConfig, error) {
volume := types.ServiceVolumeConfig{}
switch len(spec) {
case 0:
return volume, errors.New("invalid empty volume spec")
case 1, 2:
volume.Target = spec
volume.Type = string(mount.TypeVolume)
return volume, nil
}
buffer := []rune{}
for _, char := range spec {
switch {
case isWindowsDrive(char, buffer, volume):
buffer = append(buffer, char)
case char == ':':
if err := populateFieldFromBuffer(char, buffer, &volume); err != nil {
return volume, errors.Wrapf(err, "invalid spec: %s", spec)
}
buffer = []rune{}
default:
buffer = append(buffer, char)
}
}
if err := populateFieldFromBuffer(rune(0), buffer, &volume); err != nil {
return volume, errors.Wrapf(err, "invalid spec: %s", spec)
}
populateType(&volume)
return volume, nil
}
func isWindowsDrive(char rune, buffer []rune, volume types.ServiceVolumeConfig) bool {
return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0])
}
func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolumeConfig) error {
strBuffer := string(buffer)
switch {
case len(buffer) == 0:
return errors.New("empty section between colons")
// Anonymous volume
case volume.Source == "" && char == rune(0):
volume.Target = strBuffer
return nil
case volume.Source == "":
volume.Source = strBuffer
return nil
case volume.Target == "":
volume.Target = strBuffer
return nil
case char == ':':
return errors.New("too many colons")
}
for _, option := range strings.Split(strBuffer, ",") {
switch option {
case "ro":
volume.ReadOnly = true
case "nocopy":
volume.Volume = &types.ServiceVolumeVolume{NoCopy: true}
default:
if isBindOption(option) {
volume.Bind = &types.ServiceVolumeBind{Propagation: option}
} else {
return errors.Errorf("unknown option: %s", option)
}
}
}
return nil
}
func isBindOption(option string) bool {
for _, propagation := range mount.Propagations {
if mount.Propagation(option) == propagation {
return true
}
}
return false
}
func populateType(volume *types.ServiceVolumeConfig) {
switch {
// Anonymous volume
case volume.Source == "":
volume.Type = string(mount.TypeVolume)
case isFilePath(volume.Source):
volume.Type = string(mount.TypeBind)
default:
volume.Type = string(mount.TypeVolume)
}
}
func isFilePath(source string) bool {
switch source[0] {
case '.', '/', '~':
return true
}
// Windows absolute path
first, next := utf8.DecodeRuneInString(source)
if unicode.IsLetter(first) && source[next] == ':' {
return true
}
return false
}

View file

@ -0,0 +1,134 @@
package loader
import (
"testing"
"github.com/docker/docker/cli/compose/types"
"github.com/docker/docker/pkg/testutil/assert"
)
func TestParseVolumeAnonymousVolume(t *testing.T) {
for _, path := range []string{"/path", "/path/foo"} {
volume, err := parseVolume(path)
expected := types.ServiceVolumeConfig{Type: "volume", Target: path}
assert.NilError(t, err)
assert.DeepEqual(t, volume, expected)
}
}
func TestParseVolumeAnonymousVolumeWindows(t *testing.T) {
for _, path := range []string{"C:\\path", "Z:\\path\\foo"} {
volume, err := parseVolume(path)
expected := types.ServiceVolumeConfig{Type: "volume", Target: path}
assert.NilError(t, err)
assert.DeepEqual(t, volume, expected)
}
}
func TestParseVolumeTooManyColons(t *testing.T) {
_, err := parseVolume("/foo:/foo:ro:foo")
assert.Error(t, err, "too many colons")
}
func TestParseVolumeShortVolumes(t *testing.T) {
for _, path := range []string{".", "/a"} {
volume, err := parseVolume(path)
expected := types.ServiceVolumeConfig{Type: "volume", Target: path}
assert.NilError(t, err)
assert.DeepEqual(t, volume, expected)
}
}
func TestParseVolumeMissingSource(t *testing.T) {
for _, spec := range []string{":foo", "/foo::ro"} {
_, err := parseVolume(spec)
assert.Error(t, err, "empty section between colons")
}
}
func TestParseVolumeBindMount(t *testing.T) {
for _, path := range []string{"./foo", "~/thing", "../other", "/foo", "/home/user"} {
volume, err := parseVolume(path + ":/target")
expected := types.ServiceVolumeConfig{
Type: "bind",
Source: path,
Target: "/target",
}
assert.NilError(t, err)
assert.DeepEqual(t, volume, expected)
}
}
func TestParseVolumeRelativeBindMountWindows(t *testing.T) {
for _, path := range []string{
"./foo",
"~/thing",
"../other",
"D:\\path", "/home/user",
} {
volume, err := parseVolume(path + ":d:\\target")
expected := types.ServiceVolumeConfig{
Type: "bind",
Source: path,
Target: "d:\\target",
}
assert.NilError(t, err)
assert.DeepEqual(t, volume, expected)
}
}
func TestParseVolumeWithBindOptions(t *testing.T) {
volume, err := parseVolume("/source:/target:slave")
expected := types.ServiceVolumeConfig{
Type: "bind",
Source: "/source",
Target: "/target",
Bind: &types.ServiceVolumeBind{Propagation: "slave"},
}
assert.NilError(t, err)
assert.DeepEqual(t, volume, expected)
}
func TestParseVolumeWithBindOptionsWindows(t *testing.T) {
volume, err := parseVolume("C:\\source\\foo:D:\\target:ro,rprivate")
expected := types.ServiceVolumeConfig{
Type: "bind",
Source: "C:\\source\\foo",
Target: "D:\\target",
ReadOnly: true,
Bind: &types.ServiceVolumeBind{Propagation: "rprivate"},
}
assert.NilError(t, err)
assert.DeepEqual(t, volume, expected)
}
func TestParseVolumeWithInvalidVolumeOptions(t *testing.T) {
_, err := parseVolume("name:/target:bogus")
assert.Error(t, err, "invalid spec: name:/target:bogus: unknown option: bogus")
}
func TestParseVolumeWithVolumeOptions(t *testing.T) {
volume, err := parseVolume("name:/target:nocopy")
expected := types.ServiceVolumeConfig{
Type: "volume",
Source: "name",
Target: "/target",
Volume: &types.ServiceVolumeVolume{NoCopy: true},
}
assert.NilError(t, err)
assert.DeepEqual(t, volume, expected)
}
func TestParseVolumeWithReadOnly(t *testing.T) {
for _, path := range []string{"./foo", "/home/user"} {
volume, err := parseVolume(path + ":/target:ro")
expected := types.ServiceVolumeConfig{
Type: "bind",
Source: path,
Target: "/target",
ReadOnly: true,
}
assert.NilError(t, err)
assert.DeepEqual(t, volume, expected)
}
}

File diff suppressed because one or more lines are too long

View file

@ -235,7 +235,37 @@
},
"user": {"type": "string"},
"userns_mode": {"type": "string"},
"volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"volumes": {
"type": "array",
"items": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"required": ["type"],
"properties": {
"type": {"type": "string"},
"source": {"type": "string"},
"target": {"type": "string"},
"read_only": {"type": "boolean"},
"bind": {
"type": "object",
"properties": {
"propagation": {"type": "string"}
}
},
"volume": {
"type": "object",
"properties": {
"nocopy": {"type": "boolean"}
}
}
}
}
],
"uniqueItems": true
}
},
"working_dir": {"type": "string"}
},
"additionalProperties": false

View file

@ -78,18 +78,27 @@ func Validate(config map[string]interface{}, version string) error {
func toError(result *gojsonschema.Result) error {
err := getMostSpecificError(result.Errors())
description := getDescription(err)
return fmt.Errorf("%s %s", err.Field(), description)
return err
}
func getDescription(err gojsonschema.ResultError) string {
if err.Type() == "invalid_type" {
if expectedType, ok := err.Details()["expected"].(string); ok {
const (
jsonschemaOneOf = "number_one_of"
jsonschemaAnyOf = "number_any_of"
)
func getDescription(err validationError) string {
switch err.parent.Type() {
case "invalid_type":
if expectedType, ok := err.parent.Details()["expected"].(string); ok {
return fmt.Sprintf("must be a %s", humanReadableType(expectedType))
}
case jsonschemaOneOf, jsonschemaAnyOf:
if err.child == nil {
return err.parent.Description()
}
return err.child.Description()
}
return err.Description()
return err.parent.Description()
}
func humanReadableType(definition string) string {
@ -113,23 +122,45 @@ func humanReadableType(definition string) string {
return definition
}
func getMostSpecificError(errors []gojsonschema.ResultError) gojsonschema.ResultError {
var mostSpecificError gojsonschema.ResultError
type validationError struct {
parent gojsonschema.ResultError
child gojsonschema.ResultError
}
for _, err := range errors {
if mostSpecificError == nil {
mostSpecificError = err
} else if specificity(err) > specificity(mostSpecificError) {
mostSpecificError = err
} else if specificity(err) == specificity(mostSpecificError) {
func (err validationError) Error() string {
description := getDescription(err)
return fmt.Sprintf("%s %s", err.parent.Field(), description)
}
func getMostSpecificError(errors []gojsonschema.ResultError) validationError {
mostSpecificError := 0
for i, err := range errors {
if specificity(err) > specificity(errors[mostSpecificError]) {
mostSpecificError = i
continue
}
if specificity(err) == specificity(errors[mostSpecificError]) {
// Invalid type errors win in a tie-breaker for most specific field name
if err.Type() == "invalid_type" && mostSpecificError.Type() != "invalid_type" {
mostSpecificError = err
if err.Type() == "invalid_type" && errors[mostSpecificError].Type() != "invalid_type" {
mostSpecificError = i
}
}
}
return mostSpecificError
if mostSpecificError+1 == len(errors) {
return validationError{parent: errors[mostSpecificError]}
}
switch errors[mostSpecificError].Type() {
case "number_one_of", "number_any_of":
return validationError{
parent: errors[mostSpecificError],
child: errors[mostSpecificError+1],
}
default:
return validationError{parent: errors[mostSpecificError]}
}
}
func specificity(err gojsonschema.ResultError) int {

View file

@ -119,7 +119,7 @@ type ServiceConfig struct {
Tty bool `mapstructure:"tty"`
Ulimits map[string]*UlimitsConfig
User string
Volumes []string
Volumes []ServiceVolumeConfig
WorkingDir string `mapstructure:"working_dir"`
}
@ -223,6 +223,26 @@ type ServicePortConfig struct {
Protocol string
}
// ServiceVolumeConfig are references to a volume used by a service
type ServiceVolumeConfig struct {
Type string
Source string
Target string
ReadOnly bool `mapstructure:"read_only"`
Bind *ServiceVolumeBind
Volume *ServiceVolumeVolume
}
// ServiceVolumeBind are options for a service volume of type bind
type ServiceVolumeBind struct {
Propagation string
}
// ServiceVolumeVolume are options for a service volume of type volume
type ServiceVolumeVolume struct {
NoCopy bool `mapstructure:"nocopy"`
}
// ServiceSecretConfig is the secret configuration for a service
type ServiceSecretConfig struct {
Source string

View file

@ -102,7 +102,7 @@ func (m *MountOpt) Set(value string) error {
case "volume-nocopy":
volumeOptions().NoCopy, err = strconv.ParseBool(value)
if err != nil {
return fmt.Errorf("invalid value for populate: %s", value)
return fmt.Errorf("invalid value for volume-nocopy: %s", value)
}
case "volume-label":
setValueOnMap(volumeOptions().Labels, value)