diff --git a/compose/convert/volume.go b/compose/convert/volume.go index 53c50958fa..682b44377a 100644 --- a/compose/convert/volume.go +++ b/compose/convert/volume.go @@ -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 } diff --git a/compose/convert/volume_test.go b/compose/convert/volume_test.go index d218e7c2f5..705f03f404 100644 --- a/compose/convert/volume_test.go +++ b/compose/convert/volume_test.go @@ -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") } diff --git a/compose/loader/loader.go b/compose/loader/loader.go index 0b327dd428..995047e8c9 100644 --- a/compose/loader/loader.go +++ b/compose/loader/loader.go @@ -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{}{} diff --git a/compose/loader/loader_test.go b/compose/loader/loader_test.go index afa2882c32..c99ea6837c 100644 --- a/compose/loader/loader_test.go +++ b/compose/loader/loader_test.go @@ -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]) +} diff --git a/compose/loader/volume.go b/compose/loader/volume.go new file mode 100644 index 0000000000..3f33492ea7 --- /dev/null +++ b/compose/loader/volume.go @@ -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 +} diff --git a/compose/loader/volume_test.go b/compose/loader/volume_test.go new file mode 100644 index 0000000000..0735d5a54a --- /dev/null +++ b/compose/loader/volume_test.go @@ -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) + } +} diff --git a/compose/schema/bindata.go b/compose/schema/bindata.go index 0b5aa18b75..e4ef29bc72 100644 --- a/compose/schema/bindata.go +++ b/compose/schema/bindata.go @@ -89,7 +89,7 @@ func dataConfig_schema_v30Json() (*asset, error) { return a, nil } -var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\xcd\x8f\xdc\x28\x16\xbf\xd7\x5f\x61\x39\xb9\xa5\x3f\xb2\xda\x68\xa5\xcd\x6d\x8f\x7b\x9a\x39\x4f\xcb\xb1\x28\xfb\x95\x8b\x34\x06\x02\xb8\xd2\x95\xa8\xff\xf7\x11\xfe\x2a\x8c\xc1\xe0\x2e\xf7\x74\x34\x9a\x53\x77\x99\xdf\x03\xde\xf7\xe3\xc1\xcf\x5d\x92\xa4\xef\x65\x71\x84\x1a\xa5\x9f\x93\xf4\xa8\x14\xff\x7c\x7f\xff\x55\x32\x7a\xdb\x7d\xbd\x63\xa2\xba\x2f\x05\x3a\xa8\xdb\x8f\x9f\xee\xbb\x6f\xef\xd2\x1b\x4d\x87\x4b\x4d\x52\x30\x7a\xc0\x55\xde\x8d\xe4\xa7\x7f\xdf\xfd\xeb\x4e\x93\x77\x10\x75\xe6\xa0\x41\x6c\xff\x15\x0a\xd5\x7d\x13\xf0\xad\xc1\x02\x34\xf1\x43\x7a\x02\x21\x31\xa3\x69\x76\xb3\xd3\x63\x5c\x30\x0e\x42\x61\x90\xe9\xe7\x44\x6f\x2e\x49\x46\xc8\xf0\xc1\x98\x56\x2a\x81\x69\x95\xb6\x9f\x9f\xdb\x19\x92\x24\x95\x20\x4e\xb8\x30\x66\x18\xb7\xfa\xee\xfe\x32\xff\xfd\x08\xbb\xb1\x67\x35\x36\xdb\x7e\xe7\x48\x29\x10\xf4\xf7\xf9\xde\xda\xe1\x2f\x0f\xe8\xf6\xc7\xff\x6e\xff\xf8\x78\xfb\xdf\xbb\xfc\x36\xfb\xf0\x7e\x32\xac\xe5\x2b\xe0\xd0\x2d\x5f\xc2\x01\x53\xac\x30\xa3\xe3\xfa\xe9\x88\x7c\xee\xff\x7b\x1e\x17\x46\x65\xd9\x82\x11\x99\xac\x7d\x40\x44\xc2\x94\x67\x0a\xea\x3b\x13\x8f\x21\x9e\x47\xd8\x1b\xf1\xdc\xaf\xef\xe0\x79\xca\xce\x89\x91\xa6\x0e\x6a\x70\x40\xbd\x11\x33\xdd\xf2\xdb\xe8\x4f\x42\x21\x40\x85\x4d\xb6\x43\xbd\x99\xc5\xea\xe5\xaf\x63\x78\x37\x30\xbd\x88\xed\x10\xc6\xda\xed\x06\x27\xee\xed\x12\x95\xcb\xbd\xfc\xb2\x1a\x85\xe5\x91\x52\x09\x9c\xb0\xb3\xfe\xe6\x91\x47\x07\xa8\x81\xaa\x74\x14\x41\x92\xa4\xfb\x06\x93\xd2\x96\x28\xa3\xf0\x9b\x9e\xe2\xc1\xf8\x98\x24\x3f\xed\x48\x66\xcc\xd3\x8e\x4f\x7e\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x0b\x46\x15\x3c\xa9\x96\xa9\xe5\xa5\x3b\x11\xb0\xe2\x11\xc4\x01\x13\x88\xa5\x40\xa2\x92\x0b\x22\x23\x58\xaa\x9c\x89\xbc\xc4\x85\x4a\x9f\x2d\xf2\xd9\x7c\x61\x7b\x1a\x49\x8d\x5f\xd9\xce\x31\x61\x5a\x20\x9e\xa3\xb2\x9c\xf0\x81\x84\x40\xe7\xf4\x26\x49\xb1\x82\x5a\xba\x59\x4c\xd2\x86\xe2\x6f\x0d\xfc\xbf\x87\x28\xd1\x80\x3d\x6f\x29\x18\xdf\x7e\xe2\x4a\xb0\x86\xe7\x1c\x09\x6d\x60\xcb\xe2\x4f\x0b\x56\xd7\x88\x6e\x65\x75\x6b\xf8\x88\x90\x3c\xa3\x0a\x61\x0a\x22\xa7\xa8\x0e\x19\x92\xf6\x3a\xa0\xa5\xcc\xbb\x84\xbf\x68\x46\x87\xbc\xa3\x97\xd6\x04\x63\xf6\xdf\x54\x1f\x25\x5d\x32\xec\x6e\x1a\x6d\xda\x7a\x6f\xa9\x45\x98\x4b\x40\xa2\x38\xbe\x90\x9e\xd5\x08\xd3\x18\xd9\x01\x55\xe2\xcc\x19\xee\xec\xe5\x97\x33\x04\xa0\xa7\x7c\x8c\x25\xab\xc5\x00\xf4\x84\x05\xa3\xf5\xe0\x0d\x31\x01\x66\x0c\xf2\x9a\xfe\x89\x33\x09\xb6\x60\x2c\x06\xcd\xa1\x91\xd5\x89\x4c\x06\x8a\x87\x81\xf1\x9b\x24\xa5\x4d\xbd\x07\xa1\x6b\xd8\x09\xf2\xc0\x44\x8d\xf4\x66\x87\xb5\x8d\xe1\x89\xa4\x1d\x96\x67\x0a\xd0\xe4\x41\xa7\x75\x44\x72\x82\xe9\xe3\xf6\x26\x0e\x4f\x4a\xa0\xfc\xc8\xa4\x8a\x8f\xe1\x06\xf9\x11\x10\x51\xc7\xe2\x08\xc5\xe3\x02\xb9\x89\x9a\x50\x33\xa9\x62\x8c\x1c\xd7\xa8\x0a\x83\x78\x11\x82\x10\xb4\x07\xf2\x22\x3e\x37\x15\xbe\x31\x2d\xab\x2a\x0d\xf5\x59\xdc\xac\x72\xe9\x87\x43\x39\xbf\x14\xf8\x04\x22\x36\x81\x33\x7e\x29\xb8\xec\xc1\x70\x01\x92\x84\xab\xcf\x09\xf4\xcb\x5d\x57\x7c\x2e\x78\x55\xfb\x1f\x21\x69\x66\x97\x0b\x89\x95\xf7\x5d\x5f\x2c\x0e\xe3\x0a\x8a\x89\x56\x6a\x54\xe8\xba\x41\x80\xf4\xe8\xf5\x02\xed\x4f\x37\x79\xcd\x4a\x9f\x81\xce\xc0\xb6\x6c\xbc\x91\x7a\x75\x22\x4c\x5e\x54\x3f\x46\xa9\x2e\x78\x80\x08\x70\xe3\xdb\x5e\xec\x36\x2f\xdb\x0d\x9b\x58\x8b\x43\x04\x23\x09\x61\x67\xf7\x0a\x72\x32\x1b\xe6\xa7\x4f\x91\x36\xe1\xa2\xfd\xcf\x22\xad\x87\xd4\x3b\x67\x7c\x8d\x1c\x98\xea\xb2\x95\xd6\xdd\x5c\x1b\xc9\x02\xde\xf6\xca\x25\x3c\xc7\xa5\x3f\x56\xb4\x11\xc2\x74\x30\xce\x84\x9a\x79\xd7\xfa\x74\xef\xb3\x60\x53\x5c\x43\x9c\xba\x24\xfc\x6e\xf1\x99\x34\x66\xea\x8e\x22\x9a\xfb\x5f\xd0\x3f\xc2\x9e\x91\x2e\x44\x29\x07\x5a\x21\x51\xc1\xf4\x18\x82\xa9\x82\x0a\x84\x87\x80\x37\x7b\x82\xe5\x11\xca\x35\x34\x82\x29\x56\x30\x12\xe7\x18\xce\xe3\x67\xbc\x33\x4c\x27\xcc\xae\xae\xcd\xb8\xc0\x27\x4c\xa0\xb2\x38\xde\x33\x46\x00\xd1\x49\xa2\x10\x80\xca\x9c\x51\x72\x8e\x40\x4a\x85\x44\xf0\xf8\x27\xa1\x68\x04\x56\xe7\x9c\x71\xb5\x79\x55\x28\x8f\x75\x2e\xf1\x0f\x98\xfa\xde\xc5\xea\xfb\x89\x32\x6b\x43\x56\x3f\x2b\x79\x2d\xf7\xf3\x99\xed\x2b\xb9\x8d\x64\x8d\x28\xae\x73\x9c\x45\x7c\x33\x0d\x72\xcb\xe0\x6a\x0d\x78\xe6\xf0\xbd\x0a\x43\x35\xd4\xa2\xab\x38\x03\xb5\x3c\xcb\x42\xbd\xac\xb6\x96\xaa\xc4\x34\x67\x1c\x68\xd0\x37\xa4\x62\x3c\xaf\x04\x2a\x20\xe7\x20\x30\x73\x8a\x62\x12\x60\xcb\x46\x20\xbd\xfe\x7c\x1a\x89\x2b\x8a\xdc\x71\xc7\x80\xaa\x9a\x1f\x5e\xd8\x04\x50\x2a\xec\xec\x0d\xc1\x35\xf6\x3b\x8d\xc3\x6a\x23\xea\xb5\xae\x56\x73\x97\x68\x0b\xe5\x59\x54\xc8\x5e\x38\x21\x2c\x1f\x10\x22\x4e\x06\x47\x24\x56\xa4\x8e\xd6\x31\x0f\x9e\xfc\xe4\x3a\x37\x38\xf7\x35\xb9\x99\x6a\xe7\xbb\xe9\x37\x92\x39\xf1\xab\x4a\x2f\x7b\x1b\x99\xb7\xfa\x71\x3b\x55\x23\x83\x87\xb8\x16\x43\xe5\xd2\x01\x64\x84\x1a\x57\x2c\x9b\x66\x0b\x7d\xa8\xd1\x4e\x50\x62\xf7\x6e\x77\x16\x67\x2b\x2e\x49\xac\xfe\xc2\x30\x81\xab\xfb\x6f\x42\x83\xb7\x25\xcb\x37\x11\x3d\xc8\x7b\x4b\x80\x25\xda\x5b\xfd\x71\x97\x73\x6b\x6b\x14\xa7\x70\x8c\x11\xa0\x04\xb6\xf4\x32\x04\x6a\x33\x9e\x80\xfc\x35\x9b\x7c\x0a\xd7\xc0\x1a\x77\xc2\xdb\x99\xf6\xdd\x13\xa5\xc6\x2d\x4a\x40\xa9\x06\xd2\xd6\xe9\xc3\xa8\xd4\xe1\x2c\x10\x54\x5c\x8c\x93\x08\xe0\x04\x17\x48\x86\x02\xd1\x15\xcd\xa4\x86\x97\x48\x41\xde\xdd\xa2\xaf\x0a\xfd\x0b\x31\x9f\x23\x81\x08\x01\x82\x65\x1d\x13\x43\xd3\x12\x08\x3a\xbf\x28\x7d\xb6\xe4\x07\x84\x49\x23\x20\x47\x85\xea\x2f\xea\x03\x36\x97\xd6\x8c\x62\xc5\x9c\x11\x22\x6e\xc9\x1a\x3d\xe5\xc3\xb2\x2d\x24\x54\xd9\x4c\x8b\xfa\xd8\x3e\x90\x61\x09\x5d\xe1\xb7\x2e\x3b\x2f\xa8\xe8\x92\xeb\x3d\x16\x33\xac\x38\x63\x5d\x80\xd4\x91\x64\x6c\xd3\x05\xe9\x83\xa9\xa5\x3f\x65\xe4\x9c\x11\x5c\x9c\xb7\xe2\xb0\x60\xb4\x13\x72\x8c\x41\x5c\x69\x81\xda\x1c\x74\x29\x54\x73\x15\x74\xd6\x96\xe0\x3b\xa6\x25\xfb\xbe\x62\xc1\xed\x4c\x89\x13\x54\x80\x15\xef\xae\x15\xb4\x54\x02\x61\xaa\x56\xa7\xf3\x6b\xd9\xba\x22\x9b\x8f\xf6\x19\x88\xfa\x23\x2e\xfc\xea\xc1\x13\xe9\x0b\xde\x04\x7b\xb7\x35\xd4\x4c\x38\x0d\x70\x83\x67\x39\x21\x16\x07\xd8\x06\x59\x2d\xaa\xd9\xdf\xa3\x72\xc6\xb7\x3f\x6d\x84\x1b\xfa\x59\x38\x20\x61\x8e\xea\xad\xbc\x23\xfa\xfa\x23\x75\xe6\xe0\x64\xb9\x6f\x91\xf8\x7b\x17\xa1\x5d\x87\xf7\xde\x23\x64\xb3\xa7\x9e\x16\xc2\xfc\x94\xb1\x65\x53\x6c\xc3\xa0\x37\xdc\x5c\x7a\xb4\xfa\x30\xd6\xcc\x37\xa3\xac\xb2\x68\x15\x7b\xaf\x0d\xb7\xdb\x7f\x5b\xbe\xdb\x2d\x02\x57\x9d\x8f\x94\x42\xc5\x31\xea\x48\xb0\xb2\x68\xbc\x22\x0e\xf5\x4f\xd5\x02\x61\xa8\x47\xfd\x13\x85\xfe\x26\x36\xfb\xd7\xd9\x57\xff\x32\x30\xf8\x24\xaf\x45\xbd\x38\x8f\x47\xbc\x43\xfb\x05\x74\xf6\xd6\xaa\x98\xf6\x20\x0d\x95\xcc\xdb\x03\x4b\x92\x8c\xbe\x28\xed\x29\xb2\xe9\x36\x6c\x98\xe3\xf1\xf6\x34\x99\x2e\xf5\x9c\x06\x88\xe7\x2a\xc6\x5a\xb4\x17\xe2\x32\xe7\x1b\x06\x9b\xbb\x0f\x0b\x25\xc3\xd2\x83\x86\x57\xca\xb5\x1b\xf4\xf3\xdc\x3a\xb5\xce\x19\x83\x74\xe7\x0f\x72\x3d\xfe\x6f\xd0\xcf\x9e\xe7\x6a\x3e\xe9\x79\xd6\xbe\xfa\x39\xed\xc9\x76\x4f\x6b\xb3\x89\x7c\x2c\x48\xf7\x3c\xc8\x88\xee\x99\x79\xf4\xf2\xa9\xd1\xf9\x68\xd7\xee\x08\x0f\x8f\x67\x3d\x17\x20\x3b\xf3\x6f\xfb\xd0\x79\xf7\xbc\xfb\x33\x00\x00\xff\xff\xfa\xcc\x57\x15\x61\x31\x00\x00") +var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5b\xcd\x73\xdc\x28\x16\xbf\xf7\x5f\xa1\x52\x72\x8b\x3f\xb2\xb5\xa9\xad\xda\xdc\xf6\xb8\xa7\x99\xf3\xb8\x3a\x2a\x5a\x7a\xad\x26\x46\x40\x00\xb5\xdd\x49\xf9\x7f\x9f\xd2\x67\x03\x02\x81\xba\xe5\x38\x33\x35\x27\xdb\xe2\xf7\x80\xf7\xfd\x1e\xe0\x1f\x9b\x24\x49\xdf\xcb\xfc\x00\x15\x4a\x3f\x27\xe9\x41\x29\xfe\xf9\xfe\xfe\xab\x64\xf4\xb6\xfb\x7a\xc7\x44\x79\x5f\x08\xb4\x57\xb7\x1f\x3f\xdd\x77\xdf\xde\xa5\x37\x0d\x1d\x2e\x1a\x92\x9c\xd1\x3d\x2e\xb3\x6e\x24\x3b\xfe\xfb\xee\x5f\x77\x0d\x79\x07\x51\x27\x0e\x0d\x88\xed\xbe\x42\xae\xba\x6f\x02\xbe\xd5\x58\x40\x43\xfc\x90\x1e\x41\x48\xcc\x68\xba\xbd\xd9\x34\x63\x5c\x30\x0e\x42\x61\x90\xe9\xe7\xa4\xd9\x5c\x92\x8c\x90\xe1\x83\x36\xad\x54\x02\xd3\x32\x6d\x3f\xbf\xb4\x33\x24\x49\x2a\x41\x1c\x71\xae\xcd\x30\x6e\xf5\xdd\xfd\x79\xfe\xfb\x11\x76\x63\xcf\xaa\x6d\xb6\xfd\xce\x91\x52\x20\xe8\xef\xd3\xbd\xb5\xc3\x5f\x1e\xd0\xed\xf7\xff\xdd\xfe\xf1\xf1\xf6\xbf\x77\xd9\xed\xf6\xc3\x7b\x63\xb8\x91\xaf\x80\x7d\xb7\x7c\x01\x7b\x4c\xb1\xc2\x8c\x8e\xeb\xa7\x23\xf2\xa5\xff\xed\x65\x5c\x18\x15\x45\x0b\x46\xc4\x58\x7b\x8f\x88\x04\x93\x67\x0a\xea\x89\x89\xc7\x10\xcf\x23\xec\x8d\x78\xee\xd7\x77\xf0\x6c\xb2\x73\x64\xa4\xae\x82\x1a\x1c\x50\x6f\xc4\x4c\xb7\xfc\x3a\xfa\x93\x90\x0b\x50\x61\x93\xed\x50\x6f\x66\xb1\xcd\xf2\xd7\x31\xbc\x19\x98\x9e\xc5\x76\x08\x6d\xed\x76\x83\x86\x7b\xbb\x44\xe5\x72\x2f\xbf\xac\x46\x61\x79\xa4\x54\x00\x27\xec\xd4\x7c\xf3\xc8\xa3\x03\x54\x40\x55\x3a\x8a\x20\x49\xd2\x5d\x8d\x49\x61\x4b\x94\x51\xf8\xad\x99\xe2\x41\xfb\x98\x24\x3f\xec\x48\xa6\xcd\xd3\x8e\x1b\x7f\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x73\x46\x15\x3c\xab\x96\xa9\xf9\xa5\x3b\x11\xb0\xfc\x11\xc4\x1e\x13\x88\xa5\x40\xa2\x94\x33\x22\x23\x58\xaa\x8c\x89\xac\xc0\xb9\x4a\x5f\x2c\xf2\xc9\x7c\x61\x7b\x1a\x49\xb5\xbf\xb6\x1b\xc7\x84\x69\x8e\x78\x86\x8a\xc2\xe0\x03\x09\x81\x4e\xe9\x4d\x92\x62\x05\x95\x74\xb3\x98\xa4\x35\xc5\xdf\x6a\xf8\x7f\x0f\x51\xa2\x06\x7b\xde\x42\x30\xbe\xfe\xc4\xa5\x60\x35\xcf\x38\x12\x8d\x81\xcd\x8b\x3f\xcd\x59\x55\x21\xba\x96\xd5\x2d\xe1\x23\x42\xf2\x8c\x2a\x84\x29\x88\x8c\xa2\x2a\x64\x48\x8d\xd7\x01\x2d\x64\xd6\x25\xfc\x59\x33\xda\x67\x1d\xbd\xb4\x26\x18\xb3\xff\xaa\xfa\x28\xe8\x9c\x61\x77\xd3\x34\xa6\xdd\xec\x2d\xb5\x08\x33\x09\x48\xe4\x87\x0b\xe9\x59\x85\x30\x8d\x91\x1d\x50\x25\x4e\x9c\xe1\xce\x5e\x7e\x39\x43\x00\x7a\xcc\xc6\x58\xb2\x58\x0c\x40\x8f\x58\x30\x5a\x0d\xde\x10\x13\x60\xc6\x20\xdf\xd0\x3f\x73\x26\xc1\x16\x8c\xc5\xa0\x3e\x34\xb2\x6a\xc8\x64\xa0\x78\x18\x18\xbf\x49\x52\x5a\x57\x3b\x10\x4d\x0d\x6b\x20\xf7\x4c\x54\xa8\xd9\xec\xb0\xb6\x36\x6c\x48\xda\x61\x79\xba\x00\x75\x1e\x9a\xb4\x8e\x48\x46\x30\x7d\x5c\xdf\xc4\xe1\x59\x09\x94\x1d\x98\x54\xf1\x31\x5c\x23\x3f\x00\x22\xea\x90\x1f\x20\x7f\x9c\x21\xd7\x51\x06\x35\x93\x2a\xc6\xc8\x71\x85\xca\x30\x88\xe7\x21\x08\x41\x3b\x20\x17\xf1\xb9\xaa\xf0\xb5\x69\x59\x59\x36\x50\x9f\xc5\x4d\x2a\x97\x7e\x38\x94\xf3\x0b\x81\x8f\x20\x62\x13\x38\xe3\xe7\x82\xcb\x1e\x0c\x17\x20\x49\xb8\xfa\x34\xa0\x5f\xee\xba\xe2\x73\xc6\xab\xda\xdf\x08\x49\xb7\x76\xb9\x90\x58\x79\xdf\xf5\xc5\xe2\x30\xae\xa0\x30\xb4\x52\xa1\xbc\xa9\x1b\x04\x48\x8f\x5e\xcf\xd0\xbe\xbb\xc9\x2a\x56\xf8\x0c\x74\x02\xb6\x65\xe3\x8d\xd4\x8b\x13\x61\x72\x51\xfd\x18\xa5\xba\x60\x03\x11\xe0\xc6\xb7\xbd\xd8\x6d\x9e\xb7\x1b\x36\xb1\x16\x87\x08\x46\x12\xc2\xce\xee\x15\xa4\x31\x1b\xe6\xc7\x4f\x91\x36\xe1\xa2\xfd\xcf\x2c\xad\x87\xd4\x3b\x67\x7c\x8d\x1c\x98\xea\xbc\x95\xd6\xdd\x5c\x1b\xd9\x06\xbc\xed\x95\x4b\x78\x8e\x0b\x7f\xac\x68\x23\x84\xee\x60\x9c\x09\x35\xf1\xae\xe5\xe9\xde\x67\xc1\xba\xb8\x86\x38\x75\x4e\xf8\xdd\xe2\x13\x69\x4c\xd4\x1d\x45\x34\xf5\xbf\xa0\x7f\x84\x3d\x23\x9d\x89\x52\x0e\xb4\x42\xa2\x04\xb3\x0d\xc1\x54\x41\x09\xc2\x43\xc0\xeb\x1d\xc1\xf2\x00\xc5\x12\x1a\xc1\x14\xcb\x19\x89\x73\x0c\x67\xfb\x19\xef\x0c\xe6\x84\xdb\xab\x6b\x33\x2e\xf0\x11\x13\x28\x2d\x8e\x77\x8c\x11\x40\xd4\x48\x14\x02\x50\x91\x31\x4a\x4e\x11\x48\xa9\x90\x08\xb6\x7f\x12\xf2\x5a\x60\x75\xca\x18\x57\xab\x57\x85\xf2\x50\x65\x12\x7f\x07\xd3\xf7\xce\x56\xdf\x4f\xb4\xb5\x36\x64\x9d\x67\x25\xaf\xe5\x7e\x3e\xb3\x7d\x25\xb7\x91\xac\x16\xf9\x75\x8e\x33\x8b\xaf\xcd\x20\x37\x0f\x2e\x97\x80\x27\x0e\xdf\xab\x30\x54\x43\xcd\xba\x8a\x33\x50\xcb\x93\xcc\xd5\x65\xb5\xb5\x54\x05\xa6\x19\xe3\x40\x83\xbe\x21\x15\xe3\x59\x29\x50\x0e\x19\x07\x81\x99\x53\x14\x46\x80\x2d\x6a\x81\x9a\xf5\xa7\xd3\x48\x5c\x52\xe4\x8e\x3b\x1a\x54\x55\x7c\x7f\xe1\x21\x80\x52\x61\x67\xaf\x09\xae\xb0\xdf\x69\x1c\x56\x1b\x51\xaf\x75\xb5\x9a\xbb\x44\x9b\x29\xcf\xa2\x42\xf6\x4c\x87\x30\xdf\x20\x44\x74\x06\x07\x24\x16\xa4\x8e\xd6\x31\xf7\x9e\xfc\xe4\xea\x1b\x9c\xfb\x32\x6e\xa6\xda\xf9\x6e\xfa\x8d\x6c\x9d\xf8\x45\xa5\x97\xbd\x8d\xad\xb7\xfa\x71\x3b\x55\x2d\x83\x4d\x5c\x8b\xa1\x72\xae\x01\x19\xa1\xd3\x2b\x96\xe4\x2f\x11\xa1\x0d\x1d\xb5\x70\x87\x6e\x22\xe2\x78\xbf\x52\x64\xec\x7c\xed\xa8\x1f\x5d\x11\x68\x34\x3b\x3c\x39\xf0\x5d\x22\xc9\x38\x39\x8d\x28\x54\x76\xa1\x33\xba\x67\x89\x77\xbb\xfe\x22\xed\xa7\xb0\x42\x59\xce\xb8\x47\xca\xf1\x6c\x2c\xcd\x98\xd6\x29\xc4\x4c\x49\xe9\xf3\xfe\x27\x26\x1e\x9b\xdc\x52\x60\x77\x10\xd8\x58\x24\x0b\xee\x1e\xad\x63\xbb\x61\x02\xd7\xa5\x9a\x0e\x0d\x5e\x42\xce\x5f\xf0\xf5\x20\xef\xe5\x1b\x96\x68\x67\x5d\x3b\xb9\x72\x66\x13\xe4\xc5\x31\x9c\xba\x05\x28\x81\xad\x5b\x81\xa1\xfe\xd1\xd3\x34\xc8\x5f\xf3\xec\x5c\xe1\x0a\x58\xed\x8e\x28\x1b\xdd\x70\x7a\xa2\x54\xbb\x9c\x0c\x28\x55\x43\xda\x3a\x7d\x18\x95\x3a\xb4\xd8\x41\xc5\xc5\xe4\x1e\x01\x9c\xe0\x1c\xc9\x50\x7e\xbf\xe2\x8c\xb6\xe6\x05\x52\x90\x75\x8f\x53\x16\x55\x54\x33\xa5\x14\x47\x02\x11\x02\x04\xcb\x2a\xa6\x34\x49\x0b\x20\xe8\x74\x51\x55\xda\x92\xef\x11\x26\xb5\x80\x0c\xe5\xde\xc8\x6b\x51\x54\x8c\x62\xc5\x9c\x11\x22\x6e\xc9\x0a\x3d\x67\xc3\xb2\x2d\x24\xd4\x30\x98\xbd\x72\xec\xf1\xaa\x66\x09\x5d\x66\x5d\x56\xf4\xce\xa8\xe8\x5c\x42\x7b\x2c\x66\x58\x71\xc2\xba\x00\xd9\x44\x92\xf1\xf4\x3b\x48\x1f\x8c\xd9\x7d\xf3\x9e\x71\x46\x70\x7e\x5a\x8b\xc3\x9c\xd1\x4e\xc8\x31\x06\x71\xa5\x05\x36\xe6\xd0\x74\x18\x15\x57\x41\x67\x6d\x09\x9e\x30\x2d\xd8\xd3\x82\x05\xd7\x33\x25\x4e\x50\x0e\x56\xbc\xbb\x56\xd0\x52\x09\x84\xa9\x5a\x7c\xd9\x73\x2d\x5b\x57\x64\xf3\xd1\x3e\x03\x51\x7f\xc4\x05\xf3\xb8\x2f\xd2\xe7\xbc\x0e\x5e\x89\x54\x50\x31\xe1\x34\xc0\x15\x5e\xbb\x85\x58\x1c\x60\x2b\x64\xb5\xa8\x3b\xb4\x1e\x95\x31\xbe\x7e\x13\x1f\xbe\x27\xdb\x86\x03\x12\xe6\xa8\x5a\xcb\x3b\xa2\x6f\x15\x53\x67\x0e\x4e\xe6\x9b\xcd\xc4\xdf\x70\x86\x76\x1d\xde\x7b\x8f\x90\xf5\x8e\x7a\x7a\xb4\x69\x7d\xbf\xe6\x59\xf3\x8a\x41\x6f\x78\x10\xe0\xd1\xea\xc3\x58\x33\xdf\x8c\xb2\xda\x46\xab\xd8\x7b\x1b\xbf\xde\xfe\xdb\xf2\xdd\x3e\x79\x73\xd5\xf9\x48\x29\x94\x1f\xa2\x5a\x82\x85\x45\xe3\x15\x71\x68\xd2\xb8\x3a\xc3\x50\x8f\xfa\x27\x0a\xfd\x4d\x6c\xf6\xe7\xd9\x57\xff\xe0\x36\xf8\xd2\xb5\x45\x5d\x9c\xc7\x23\x9e\x77\xfe\x02\x3a\x7b\x6b\x55\x98\x47\xfb\x9a\x4a\xa6\xc7\x03\x73\x92\x8c\x7e\x7f\xd0\x53\x6c\xcd\x6d\xd8\x30\xc7\xff\x44\x98\xc9\x74\xee\xe2\x6f\x80\x78\x8e\xa3\xac\x45\x7b\x21\xce\x73\xbe\x62\xb0\xb9\xfb\x30\x53\x32\xcc\xbd\x13\x7a\xa5\x5c\xbb\xc2\xa5\xaa\x5b\xa7\x56\x9f\x31\x48\x77\xfa\xce\xdd\xe3\xff\x1a\xfd\xe4\xd5\x7b\xc3\x27\x3d\x4d\x8e\xaf\x7e\x98\xc7\xe8\xdd\x8b\xf5\xad\x21\x1f\x0b\xd2\xbd\xba\xd3\xa2\xfb\x56\x6f\xbd\x7c\x6a\x74\xbe\x85\xb7\x0f\xf1\x87\x37\xe9\x9e\x7b\xc5\x8d\xfe\xb3\xfd\xff\x81\xcd\xcb\xe6\xcf\x00\x00\x00\xff\xff\xea\x87\x24\xae\xb8\x34\x00\x00") func dataConfig_schema_v31JsonBytes() ([]byte, error) { return bindataRead( diff --git a/compose/schema/data/config_schema_v3.1.json b/compose/schema/data/config_schema_v3.1.json index b9d4221995..c5e48968e3 100644 --- a/compose/schema/data/config_schema_v3.1.json +++ b/compose/schema/data/config_schema_v3.1.json @@ -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 diff --git a/compose/schema/schema.go b/compose/schema/schema.go index ae33c77fbe..9a70dc2aaa 100644 --- a/compose/schema/schema.go +++ b/compose/schema/schema.go @@ -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 { diff --git a/compose/types/types.go b/compose/types/types.go index ba11faa138..dce13c928a 100644 --- a/compose/types/types.go +++ b/compose/types/types.go @@ -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