From b3459936dba6f0fb5b23b43cd5e168186ad87f35 Mon Sep 17 00:00:00 2001 From: Li Yi Date: Sat, 6 May 2017 21:49:42 +0800 Subject: [PATCH] Support placement preferences in stack deployment Move of moby/moby#32743 Signed-off-by: Li Yi --- cli/compose/convert/service.go | 14 + cli/compose/loader/full-example.yml | 4 +- cli/compose/loader/loader_test.go | 5 + cli/compose/schema/bindata.go | 33 +- .../schema/data/config_schema_v3.3.json | 485 ++++++++++++++++++ cli/compose/schema/schema_test.go | 24 + cli/compose/types/types.go | 6 + 7 files changed, 565 insertions(+), 6 deletions(-) create mode 100644 cli/compose/schema/data/config_schema_v3.3.json diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index fadf69b5bfe8..a7fc3e5bd486 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -135,6 +135,7 @@ func convertService( RestartPolicy: restartPolicy, Placement: &swarm.Placement{ Constraints: service.Deploy.Placement.Constraints, + Preferences: getPlacementPreference(service.Deploy.Placement.Preferences), }, }, EndpointSpec: endpoint, @@ -157,6 +158,19 @@ func convertService( return serviceSpec, nil } +func getPlacementPreference(preferences []composetypes.PlacementPreferences) []swarm.PlacementPreference { + result := []swarm.PlacementPreference{} + for _, preference := range preferences { + spreadDescriptor := preference.Spread + result = append(result, swarm.PlacementPreference{ + Spread: &swarm.SpreadOver{ + SpreadDescriptor: spreadDescriptor, + }, + }) + } + return result +} + func sortStrings(strs []string) []string { sort.Strings(strs) return strs diff --git a/cli/compose/loader/full-example.yml b/cli/compose/loader/full-example.yml index e8a61785dfd1..e1e3b77d395d 100644 --- a/cli/compose/loader/full-example.yml +++ b/cli/compose/loader/full-example.yml @@ -1,4 +1,4 @@ -version: "3.2" +version: "3.3" services: foo: @@ -45,6 +45,8 @@ services: window: 120s placement: constraints: [node=foo] + preferences: + - spread: node.labels.az endpoint_mode: dnsrr devices: diff --git a/cli/compose/loader/loader_test.go b/cli/compose/loader/loader_test.go index 15801f2b50f6..12e00e1de229 100644 --- a/cli/compose/loader/loader_test.go +++ b/cli/compose/loader/loader_test.go @@ -673,6 +673,11 @@ func TestFullExample(t *testing.T) { }, Placement: types.Placement{ Constraints: []string{"node=foo"}, + Preferences: []types.PlacementPreferences{ + { + Spread: "node.labels.az", + }, + }, }, EndpointMode: "dnsrr", }, diff --git a/cli/compose/schema/bindata.go b/cli/compose/schema/bindata.go index c8ff76693f8b..1102e5cffc64 100644 --- a/cli/compose/schema/bindata.go +++ b/cli/compose/schema/bindata.go @@ -3,6 +3,7 @@ // data/config_schema_v3.0.json // data/config_schema_v3.1.json // data/config_schema_v3.2.json +// data/config_schema_v3.3.json // DO NOT EDIT! package schema @@ -130,6 +131,26 @@ func dataConfig_schema_v32Json() (*asset, error) { return a, nil } +var _dataConfig_schema_v33Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5b\x4b\x6f\xdb\xb8\x13\xbf\xfb\x53\x08\x6a\x6f\xcd\xa3\x40\x8b\x3f\xf0\xef\x6d\x8f\x7b\xda\x3d\x6f\xe0\x0a\x34\x35\x96\xd9\x48\x24\x4b\x52\x4e\xdc\x22\xdf\x7d\xa1\xa7\x49\x8a\x14\xa9\x58\x69\xb2\x8b\x3d\xb5\x91\x66\x86\x9c\x27\x7f\xc3\x91\x7f\x6e\x92\x24\x7d\x2f\xf1\x01\x2a\x94\x7e\x49\xd2\x83\x52\xfc\xcb\xed\xed\x37\xc9\xe8\x75\xf7\xf4\x86\x89\xe2\x36\x17\x68\xaf\xae\x3f\x7e\xbe\xed\x9e\xbd\x4b\xaf\x1a\x3e\x92\x37\x2c\x98\xd1\x3d\x29\xb2\xee\x4d\x76\xfc\x74\xf3\xe9\xa6\x61\xef\x48\xd4\x89\x43\x43\xc4\x76\xdf\x00\xab\xee\x99\x80\xef\x35\x11\xd0\x30\xdf\xa5\x47\x10\x92\x30\x9a\x6e\xaf\x36\xcd\x3b\x2e\x18\x07\xa1\x08\xc8\xf4\x4b\xd2\x6c\x2e\x49\x46\x92\xe1\x81\x26\x56\x2a\x41\x68\x91\xb6\x8f\x9f\x5a\x09\x49\x92\x4a\x10\x47\x82\x35\x09\xe3\x56\xdf\xdd\x9e\xe5\xdf\x8e\x64\x57\xb6\x54\x6d\xb3\xed\x73\x8e\x94\x02\x41\xff\x9c\xee\xad\x7d\xfd\xf5\x0e\x5d\xff\xf8\xed\xfa\xaf\x8f\xd7\xff\xbf\xc9\xae\xb7\x1f\xde\x1b\xaf\x1b\xfb\x0a\xd8\x77\xcb\xe7\xb0\x27\x94\x28\xc2\xe8\xb8\x7e\x3a\x52\x3e\xf5\xff\x7b\x1a\x17\x46\x79\xde\x12\xa3\xd2\x58\x7b\x8f\x4a\x09\xa6\xce\x14\xd4\x03\x13\xf7\x21\x9d\x47\xb2\x57\xd2\xb9\x5f\xdf\xa1\xb3\xa9\xce\x91\x95\x75\x15\xf4\xe0\x40\xf5\x4a\xca\x74\xcb\xaf\xe3\x3f\x09\x58\x80\x0a\x87\x6c\x47\xf5\x6a\x11\xdb\x2c\x7f\x99\xc2\x9b\x41\xe9\x59\xda\x8e\x42\x5b\xbb\xdd\xa0\x91\xde\x2e\x53\xb9\xd2\xcb\x6f\xab\xd1\x58\x1e\x2b\xe5\xc0\x4b\x76\x6a\x9e\x79\xec\xd1\x11\x54\x40\x55\x3a\x9a\x20\x49\xd2\x5d\x4d\xca\xdc\xb6\x28\xa3\xf0\x47\x23\xe2\x4e\x7b\x98\x24\x3f\xed\x4a\xa6\xc9\x69\xdf\x1b\x7f\xf9\x1d\x3e\xbe\xf7\xe8\x32\xbe\xc7\x8c\x2a\x78\x54\xad\x52\xf3\x4b\x77\x26\x60\xf8\x1e\xc4\x9e\x94\x10\xcb\x81\x44\x21\x67\x4c\x56\x12\xa9\x32\x26\xb2\x9c\x60\xe5\xe4\x2f\xd1\x0e\xca\x8b\x24\x60\x84\x0f\x90\xed\x05\xab\x82\x52\xf6\x59\xa7\x89\x4c\x9f\x2c\x39\x13\xc1\xe1\xd0\x1e\x59\xb5\xbf\xb6\x1b\x87\xc0\x14\x23\x9e\xa1\x3c\x37\x4c\x8a\x84\x40\xa7\xf4\x2a\x49\x89\x82\x4a\xba\xad\x9d\xa4\x35\x25\xdf\x6b\xf8\xbd\x27\x51\xa2\x06\x5b\x6e\x2e\x18\x5f\x5f\x70\x21\x58\xcd\x33\x8e\x44\x13\xeb\xf3\x91\x90\x62\x56\x55\x88\xae\x95\x00\x4b\xf4\x88\xb0\x3c\xa3\x0a\x11\x0a\x22\xa3\xa8\x0a\xc5\x74\x53\x00\x80\xe6\x32\xeb\xb0\x47\x6c\x24\x19\x02\x46\x20\xb2\xaa\x3f\x72\x3a\x97\x21\x9d\x98\x26\x47\x9a\xbd\xa5\x16\x63\x26\x01\x09\x7c\x78\x26\x3f\xab\x10\xa1\x31\xb6\x03\xaa\xc4\x89\x33\xd2\xc5\xcb\x9b\x0b\x04\xa0\xc7\x6c\x2c\x6b\x8b\xcd\x00\xf4\x48\x04\xa3\xd5\x90\x0d\x71\x95\x4a\xe3\x7f\xe4\x4c\x82\x6d\x18\x4b\x41\xfd\xd5\xa8\xaa\x61\x93\x81\xe3\x6e\x50\xfc\x2a\x49\x69\x5d\xed\x40\x34\x70\xda\xa0\xdc\x33\x51\xa1\x66\xb3\xc3\xda\xda\x6b\xc3\xd2\x8e\xc8\xd3\x0d\xa8\xeb\xd0\x20\x0c\x54\x66\x25\xa1\xf7\xeb\x87\x38\x3c\x2a\x81\xb2\x03\x93\xea\x39\x87\x41\x7a\x00\x54\xaa\x03\x3e\x00\xbe\x9f\x61\xd7\xa9\x0c\x6e\x26\x55\x4c\x90\x93\x0a\x15\x61\x22\x8e\x43\x24\xcf\x3e\xf4\xd2\x55\x8d\xaf\x89\x65\x45\xd1\x90\xfa\x22\x6e\x02\xa2\xfa\xd7\x21\xf8\x91\x0b\x72\x04\x11\x8b\x25\x18\x3f\x63\x3f\xfb\x65\x18\x0b\x25\x61\x20\x6c\x90\x7e\xbd\xe9\x70\xf0\x4c\x56\xb5\xff\x2b\xcb\x74\x6b\xc3\x85\xc4\x3a\xf7\x5d\x4f\x2c\x0d\xe3\x00\x85\xe1\x95\x0a\xe1\x06\x37\x08\x90\x1e\xbf\x9e\x49\xfb\x46\x2b\xab\x58\xee\x0b\xd0\x09\xb1\x6d\x1b\x6f\xa5\x5e\x7c\x10\x26\xcf\x82\xb2\x51\xae\x0b\xf6\x32\x01\x6d\x7c\xdb\x8b\xdd\xe6\x79\xbb\xe1\x10\x6b\xe9\x50\x49\x90\x84\x70\xb2\x7b\x0d\x69\x48\x23\xfc\xf8\x39\x32\x26\x5c\xbc\xff\x9b\xe5\xf5\xb0\x7a\x65\xc6\x63\xe4\x80\xa8\xf3\x56\xda\x74\x73\x6d\x64\x1b\xc8\xb6\x17\x86\xf0\x9c\xe4\xfe\x5a\xd1\x56\x08\x3d\xc1\x38\x13\x6a\x92\x5d\xcb\x8f\x7b\x5f\x04\xeb\xe6\x1a\xea\xd4\xf9\xc0\xef\x16\x9f\x58\x63\xe2\xee\x28\xa6\x69\xfe\x05\xf3\x23\x9c\x19\xe9\x4c\x95\x72\x50\x2b\x24\x0a\x30\xdb\x10\x42\x15\x14\x20\x3c\x0c\xbc\xde\x95\x44\x1e\x20\x5f\xc2\x23\x98\x62\x98\x95\x71\x89\xe1\xec\x84\xe3\x93\xc1\x14\xb8\xbd\x18\x9b\x71\x41\x8e\xa4\x84\xc2\xd2\x78\xc7\x58\x09\x88\x1a\x07\x85\x00\x94\x67\x8c\x96\xa7\x08\x4a\xa9\x90\x08\xb6\x7f\x12\x70\x2d\x88\x3a\x65\x8c\xab\xd5\x51\xa1\x3c\x54\x99\x24\x3f\xc0\xcc\xbd\x73\xd4\xf7\x82\xb6\xd6\x86\xac\xab\xb5\xe4\xa5\xd2\xcf\x17\xb6\x2f\x94\x36\x92\xd5\x02\x5f\x96\x38\xb3\xf4\xb5\x59\xe4\xe6\x89\x8b\x25\xc4\x93\x84\xef\x5d\x18\xc2\x50\xb3\xa9\xe2\x2c\xd4\xf2\x24\xb1\x7a\x1e\xb6\x96\x2a\x27\x34\x63\x1c\x68\x30\x37\xa4\x62\x3c\x2b\x04\xc2\x90\x71\x10\x84\x39\x4d\x61\x14\xd8\xbc\x16\xa8\x59\x7f\x2a\x46\x92\x82\x22\x77\xdd\xd1\x48\x55\xc5\xf7\xcf\xbc\x04\x50\x2a\x9c\xec\x75\x49\x2a\xe2\x4f\x1a\x47\xd4\x46\xe0\xb5\x0e\xab\xb9\x21\xda\x0c\x3c\x8b\x2a\xd9\x33\x1d\xc2\x7c\x83\x10\xd1\x19\x1c\x90\x58\x70\x74\xb4\x89\xb9\xf7\x9c\x4f\xae\xbe\xc1\xb9\x2f\x63\x48\xd6\xca\xbb\xea\x37\xb2\x75\xd2\x2f\x82\x5e\xf6\x36\xb6\x5e\xf4\xe3\x4e\xaa\x5a\x06\x9b\xb8\x96\x86\xca\xb9\x06\x64\x24\x9d\x4e\x7b\x92\x7f\x44\x85\x36\x7c\xd4\x92\x3b\x7c\x13\x51\xc7\xfb\x95\x22\x6b\xe7\x4b\x57\xfd\x68\x44\xa0\xf1\x60\x46\x25\x91\x0a\x28\x3e\xc5\x2f\xb4\x23\x93\x5b\xe2\xa9\x51\xe6\xfb\xae\xb8\xae\xab\xa5\x42\x45\x57\x6f\xa3\x1b\x9d\xf8\x5c\xed\x07\x81\xbf\x44\x15\xca\x30\xe3\x1e\xd7\xc4\xab\xb1\xf4\x98\xb5\xae\x2e\x66\x70\xa8\xaf\x64\x3c\x30\x71\xdf\x1c\x48\x39\x71\x57\x8e\x8d\xc5\xb2\x60\x76\x6a\xdd\xf5\x0d\x02\x5c\x43\x41\x9d\x34\x38\x44\x9d\x1f\x50\xf6\x44\xde\xe1\x21\x91\x68\x67\x8d\xcd\x5c\x07\x6d\x73\x32\x88\x63\xf8\xbc\x17\xa0\x04\xb1\x46\x09\x03\x68\xd2\xcf\x76\x90\x6f\xf3\xc2\x5d\x91\x0a\x58\xed\x2e\x43\x1b\x3d\x70\x7a\xa6\x54\x1b\xae\x06\x9c\xaa\x51\xda\x3e\xbd\x1b\x9d\x3a\xf4\xe5\x41\xc7\xc5\x1c\x58\x40\xf3\x76\xb4\x11\x75\xba\x09\xe0\x25\xc1\x48\x86\x10\xc4\x05\xb7\xc0\x35\xcf\x91\x82\xac\xfb\x12\x67\x11\x66\x9b\x01\x6b\x1c\x09\x54\x96\x50\x12\x59\xc5\x80\x9f\x34\x87\x12\x39\xab\x7f\x10\xf7\xb6\xec\x7b\x44\xca\x5a\x40\x86\xb0\xb7\x4c\x5b\x1c\x15\xa3\x44\x31\x67\x39\x89\x5b\xb2\x42\x8f\xd9\xb0\x6c\x4b\x12\x6a\x49\xcc\x6e\x3c\xf6\x02\x57\x8b\x84\xee\xec\x5e\x06\xab\x67\x5c\x74\x06\xe9\x9e\x88\x19\x56\x9c\xa8\x2e\x40\x36\x65\x67\xbc\x5f\x0f\xf2\x07\x0b\x7c\x7f\x3d\x90\x71\x56\x92\x0e\x05\xac\xa1\x21\x66\xb4\x33\x72\x4c\x40\x5c\x18\x81\x4d\x38\x34\x3d\x4c\xc5\x55\x30\x59\x5b\x86\x07\x42\x73\xf6\xb0\x60\xc1\xf5\x42\x89\x97\x08\x83\x55\x1c\x2f\x35\xb4\x54\x02\x11\xaa\x16\x8f\x93\x6c\xb3\x70\x01\x7b\x10\x40\xa7\x81\x9e\xcc\xc3\xfa\xc4\x0f\xed\x43\xba\x85\x35\xec\x29\x24\x6f\xf0\xed\x2b\xdc\xea\x5d\xea\xfc\x0b\x00\xd2\x98\xc5\x81\x83\x74\xa4\x0b\x42\x23\xdf\xe1\x89\x79\x1d\x1c\x4d\x55\x50\x31\xe1\x6e\x13\x2e\xd0\x71\xf8\xb2\x30\xa0\xe2\x40\xb6\x02\x50\x88\x9a\x65\xf6\x54\x19\xe3\xeb\x5f\xa6\x84\xe7\x95\xdb\x70\xd9\x26\x1c\x55\x6b\xd5\x90\xe8\xe9\x6e\xea\x44\x2a\xc9\x1b\xa8\x0e\xf5\x8e\x7a\x7a\xe5\xb7\x5d\x1d\xcc\x2f\x27\xda\x0f\x33\x3c\x5e\xbd\x1b\xdb\x90\xab\xd1\x56\xdb\x68\x17\x7b\xbf\x8a\x58\x6f\xff\x6d\x47\x64\xdf\x80\xba\x5a\x27\xa4\x14\xc2\x87\xa8\x2e\x6b\x21\xb4\xbe\xa0\x0e\x4d\xee\x02\x9c\x65\xa8\xa7\xfa\xaf\x0a\xfd\x4b\x62\xf6\xd7\xc5\x57\xff\x0d\x76\xf0\xe3\xe7\x96\xea\xd9\xe7\x78\xc4\x17\xbf\x6f\xc0\x67\xaf\xed\x0a\x73\xc4\xa2\xb9\x64\x7a\xe3\x32\x67\xc9\xa5\xdf\x28\x6f\xcd\x6d\xd8\x64\x8e\x9f\xc9\x98\x87\xe9\xdc\x00\x76\x20\xf1\xdc\xf0\x59\x8b\xf6\x46\x9c\xd7\x7c\xc5\x62\x73\xf3\x61\x06\x32\xcc\x7d\xaf\xf5\x42\x67\xed\x0a\xc3\x6d\xb7\x4f\xad\x6e\x6c\xb0\xee\xf4\xa7\x0f\x9e\xfc\xd7\xf8\x27\x3f\x84\x68\xf4\xa4\xa7\xc9\x8d\xe0\x4f\x73\x9c\xd1\xfd\x88\x61\x6b\xd8\xc7\x22\xe9\xbe\x7e\xd4\xaa\xfb\x56\x6f\x50\x7d\x6e\x74\xfe\x3c\xc2\x1e\xa6\x0c\x3f\x53\xf0\xcc\x77\x37\xfa\xbf\xed\x4f\x4a\x36\x4f\x9b\xbf\x03\x00\x00\xff\xff\xca\x6f\x19\x50\xcb\x36\x00\x00") + +func dataConfig_schema_v33JsonBytes() ([]byte, error) { + return bindataRead( + _dataConfig_schema_v33Json, + "data/config_schema_v3.3.json", + ) +} + +func dataConfig_schema_v33Json() (*asset, error) { + bytes, err := dataConfig_schema_v33JsonBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "data/config_schema_v3.3.json", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. @@ -185,6 +206,7 @@ var _bindata = map[string]func() (*asset, error){ "data/config_schema_v3.0.json": dataConfig_schema_v30Json, "data/config_schema_v3.1.json": dataConfig_schema_v31Json, "data/config_schema_v3.2.json": dataConfig_schema_v32Json, + "data/config_schema_v3.3.json": dataConfig_schema_v33Json, } // AssetDir returns the file names below a certain @@ -226,12 +248,12 @@ type bintree struct { Func func() (*asset, error) Children map[string]*bintree } - var _bintree = &bintree{nil, map[string]*bintree{ - "data": {nil, map[string]*bintree{ - "config_schema_v3.0.json": {dataConfig_schema_v30Json, map[string]*bintree{}}, - "config_schema_v3.1.json": {dataConfig_schema_v31Json, map[string]*bintree{}}, - "config_schema_v3.2.json": {dataConfig_schema_v32Json, map[string]*bintree{}}, + "data": &bintree{nil, map[string]*bintree{ + "config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}}, + "config_schema_v3.1.json": &bintree{dataConfig_schema_v31Json, map[string]*bintree{}}, + "config_schema_v3.2.json": &bintree{dataConfig_schema_v32Json, map[string]*bintree{}}, + "config_schema_v3.3.json": &bintree{dataConfig_schema_v33Json, map[string]*bintree{}}, }}, }} @@ -281,3 +303,4 @@ func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } + diff --git a/cli/compose/schema/data/config_schema_v3.3.json b/cli/compose/schema/data/config_schema_v3.3.json new file mode 100644 index 000000000000..efdd41625f7f --- /dev/null +++ b/cli/compose/schema/data/config_schema_v3.3.json @@ -0,0 +1,485 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "config_schema_v3.3.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 + }, + + "secrets": { + "id": "#/properties/secrets", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/secret" + } + }, + "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"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "cache_from": {"$ref": "#/definitions/list_of_strings"} + }, + "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": { + "oneOf": [ + {"type": "number", "format": "ports"}, + {"type": "string", "format": "ports"}, + { + "type": "object", + "properties": { + "mode": {"type": "string"}, + "target": {"type": "integer"}, + "published": {"type": "integer"}, + "protocol": {"type": "string"} + }, + "additionalProperties": false + } + ] + }, + "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"]}, + "secrets": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "source": {"type": "string"}, + "target": {"type": "string"}, + "uid": {"type": "string"}, + "gid": {"type": "string"}, + "mode": {"type": "number"} + } + } + ] + } + }, + "sysctls": {"$ref": "#/definitions/list_or_dict"}, + "stdin_open": {"type": "boolean"}, + "stop_grace_period": {"type": "string", "format": "duration"}, + "stop_signal": {"type": "string"}, + "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"}, + "userns_mode": {"type": "string"}, + "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"}, + "consistency": {"type": "string"}, + "bind": { + "type": "object", + "properties": { + "propagation": {"type": "string"} + } + }, + "volume": { + "type": "object", + "properties": { + "nocopy": {"type": "boolean"} + } + } + } + } + ], + "uniqueItems": true + } + }, + "working_dir": {"type": "string"} + }, + "additionalProperties": false + }, + + "healthcheck": { + "id": "#/definitions/healthcheck", + "type": "object", + "additionalProperties": false, + "properties": { + "disable": {"type": "boolean"}, + "interval": {"type": "string"}, + "retries": {"type": "number"}, + "test": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "timeout": {"type": "string"} + } + }, + "deployment": { + "id": "#/definitions/deployment", + "type": ["object", "null"], + "properties": { + "mode": {"type": "string"}, + "endpoint_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"}}, + "preferences": { + "type": "array", + "items": { + "type": "object", + "properties": { + "spread": {"type": "string"} + }, + "additionalProperties": false + } + } + }, + "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 + }, + "internal": {"type": "boolean"}, + "attachable": {"type": "boolean"}, + "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"} + }, + "additionalProperties": false + }, + "labels": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false + }, + + "secret": { + "id": "#/definitions/secret", + "type": "object", + "properties": { + "file": {"type": "string"}, + "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"] + } + } + } + } + } +} diff --git a/cli/compose/schema/schema_test.go b/cli/compose/schema/schema_test.go index 0935d4022e48..f293fe7f68f0 100644 --- a/cli/compose/schema/schema_test.go +++ b/cli/compose/schema/schema_test.go @@ -50,3 +50,27 @@ func TestValidateInvalidVersion(t *testing.T) { assert.Error(t, err) assert.Contains(t, err.Error(), "unsupported Compose file version: 2.1") } + +type array []interface{} + +func TestValidatePlacement(t *testing.T) { + config := dict{ + "version": "3.3", + "services": dict{ + "foo": dict{ + "image": "busybox", + "deploy": dict{ + "placement": dict{ + "preferences": array{ + dict{ + "spread": "node.labels.az", + }, + }, + }, + }, + }, + }, + } + + assert.NoError(t, Validate(config, "3.3")) +} diff --git a/cli/compose/types/types.go b/cli/compose/types/types.go index f973a2bc1f41..d9be14d22309 100644 --- a/cli/compose/types/types.go +++ b/cli/compose/types/types.go @@ -208,6 +208,12 @@ type RestartPolicy struct { // Placement constraints for the service type Placement struct { Constraints []string + Preferences []PlacementPreferences +} + +// PlacementPreferences is the preferences for a service placement +type PlacementPreferences struct { + Spread string } // ServiceNetworkConfig is the network configuration for a service