diff --git a/api/handlers.go b/api/handlers.go index 7a2500c5bf..a5e468331c 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -195,7 +195,7 @@ func getContainerJSON(c *context, w http.ResponseWriter, r *http.Request) { func postContainersCreate(c *context, w http.ResponseWriter, r *http.Request) { r.ParseForm() var ( - config cluster.ContainerConfig + config dockerclient.ContainerConfig name = r.Form.Get("name") ) @@ -209,7 +209,7 @@ func postContainersCreate(c *context, w http.ResponseWriter, r *http.Request) { return } - container, err := c.cluster.CreateContainer(&config, name) + container, err := c.cluster.CreateContainer(cluster.BuildContainerConfig(&config), name) if err != nil { httpError(w, err.Error(), http.StatusInternalServerError) return diff --git a/cluster/config.go b/cluster/config.go index f2163d2c7e..762c9e8a1f 100644 --- a/cluster/config.go +++ b/cluster/config.go @@ -1,8 +1,98 @@ package cluster -import "github.com/samalba/dockerclient" +import ( + "encoding/json" + "strings" + + "github.com/samalba/dockerclient" +) + +const namespace = "com.docker.swarm" // ContainerConfig is exported +// TODO store affinities and constraints in their own fields type ContainerConfig struct { dockerclient.ContainerConfig } + +func parseEnv(e string) (bool, string, string) { + parts := strings.SplitN(e, ":", 2) + if len(parts) == 2 { + return true, parts[0], parts[1] + } + return false, "", "" +} + +// BuildContainerConfig creates a cluster.ContainerConfig from a dockerclient.ContainerConfig +func BuildContainerConfig(c *dockerclient.ContainerConfig) *ContainerConfig { + var ( + affinities []string + constraints []string + env []string + ) + + // only for tests + if c.Labels == nil { + c.Labels = make(map[string]string) + } + + // parse affinities from labels (ex. docker run --label 'com.docker.swarm.affinities=["container==redis","image==nginx"]') + if labels, ok := c.Labels[namespace+".affinities"]; ok { + json.Unmarshal([]byte(labels), &affinities) + } + + // parse contraints from labels (ex. docker run --label 'com.docker.swarm.constraints=["region==us-east","storage==ssd"]') + if labels, ok := c.Labels[namespace+".constraints"]; ok { + json.Unmarshal([]byte(labels), &constraints) + } + + // parse affinities/contraints from env (ex. docker run -e affinity:container==redis -e affinity:image==nginx -e constraint:region==us-east -e constraint:storage==ssd) + for _, e := range c.Env { + if ok, key, value := parseEnv(e); ok && key == "affinity" { + affinities = append(affinities, value) + } else if ok && key == "constraint" { + constraints = append(constraints, value) + } else { + env = append(env, e) + } + } + + // remove affinities/contraints from env + c.Env = env + + // store affinities in labels + if len(affinities) > 0 { + if labels, err := json.Marshal(affinities); err == nil { + c.Labels[namespace+".affinities"] = string(labels) + } + } + + // store contraints in labels + if len(constraints) > 0 { + if labels, err := json.Marshal(constraints); err == nil { + c.Labels[namespace+".constraints"] = string(labels) + } + } + + return &ContainerConfig{*c} +} + +func (c *ContainerConfig) extractExprs(key string) []string { + var exprs []string + + if labels, ok := c.Labels[namespace+"."+key]; ok { + json.Unmarshal([]byte(labels), &exprs) + } + + return exprs +} + +// Affinities returns all the affinities from the ContainerConfig +func (c *ContainerConfig) Affinities() []string { + return c.extractExprs("affinities") +} + +// Constraints returns all the constraints from the ContainerConfig +func (c *ContainerConfig) Constraints() []string { + return c.extractExprs("constraints") +} diff --git a/cluster/config_test.go b/cluster/config_test.go new file mode 100644 index 0000000000..8d74750187 --- /dev/null +++ b/cluster/config_test.go @@ -0,0 +1,52 @@ +package cluster + +import ( + "testing" + + "github.com/samalba/dockerclient" + "github.com/stretchr/testify/assert" +) + +func TestBuildContainerConfig(t *testing.T) { + config := BuildContainerConfig(&dockerclient.ContainerConfig{}) + assert.Equal(t, len(config.Env), 0) + assert.Equal(t, len(config.Labels), 0) + + config = BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"test=true"}}) + assert.Equal(t, len(config.Env), 1) + assert.Equal(t, len(config.Labels), 0) + + config = BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:test==true"}}) + assert.Equal(t, len(config.Env), 0) + assert.Equal(t, len(config.Labels), 1) + + config = BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:container==test"}}) + assert.Equal(t, len(config.Env), 0) + assert.Equal(t, len(config.Labels), 1) + + config = BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"test=true", "constraint:test==true", "affinity:container==test"}}) + assert.Equal(t, len(config.Env), 1) + assert.Equal(t, len(config.Labels), 2) +} + +func TestConstraints(t *testing.T) { + config := BuildContainerConfig(&dockerclient.ContainerConfig{}) + assert.Equal(t, len(config.Constraints()), 0) + + config = BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:test==true"}}) + assert.Equal(t, len(config.Constraints()), 1) + + config = BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"test=true", "constraint:test==true", "affinity:container==test"}}) + assert.Equal(t, len(config.Constraints()), 1) +} + +func TestAffinities(t *testing.T) { + config := BuildContainerConfig(&dockerclient.ContainerConfig{}) + assert.Equal(t, len(config.Affinities()), 0) + + config = BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:container==test"}}) + assert.Equal(t, len(config.Affinities()), 1) + + config = BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"test=true", "constraint:test==true", "affinity:container==test"}}) + assert.Equal(t, len(config.Affinities()), 1) +} diff --git a/scheduler/filter/affinity.go b/scheduler/filter/affinity.go index e3262ad356..2f50b48415 100644 --- a/scheduler/filter/affinity.go +++ b/scheduler/filter/affinity.go @@ -20,7 +20,7 @@ func (f *AffinityFilter) Name() string { // Filter is exported func (f *AffinityFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { - affinities, err := parseExprs("affinity", config.Env) + affinities, err := parseExprs(config.Affinities()) if err != nil { return nil, err } diff --git a/scheduler/filter/affinity_test.go b/scheduler/filter/affinity_test.go index 7efd31a7c3..91a21b90c9 100644 --- a/scheduler/filter/affinity_test.go +++ b/scheduler/filter/affinity_test.go @@ -67,182 +67,128 @@ func TestAffinityFilter(t *testing.T) { assert.Equal(t, result, nodes) // Set a constraint that cannot be fulfilled and expect an error back. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:container==does_not_exsits"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:container==does_not_exsits"}}), nodes) assert.Error(t, err) // Set a constraint that can only be filled by a single node. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:container==container-n0*"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:container==container-n0*"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) // This constraint can only be fulfilled by a subset of nodes. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:container==container-*"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:container==container-*"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) assert.NotContains(t, result, nodes[2]) // Validate by id. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:container==container-n0-0-id"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:container==container-n0-0-id"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) // Validate by id. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:container!=container-n0-0-id"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n0-0-id"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) assert.NotContains(t, result, nodes[0]) // Validate by id. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:container!=container-n0-1-id"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n0-1-id"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) assert.NotContains(t, result, nodes[0]) // Validate by name. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:container==container-n1-0-name"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:container==container-n1-0-name"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) // Validate by name. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:container!=container-n1-0-name"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n1-0-name"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) assert.NotContains(t, result, nodes[1]) // Validate by name. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:container!=container-n1-1-name"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n1-1-name"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) assert.NotContains(t, result, nodes[1]) // Validate images by id - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:image==image-0-id"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:image==image-0-id"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) // Validate images by name - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:image==image-0:tag3"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:image==image-0:tag3"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) // Validate images by name - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:image!=image-0:tag3"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:image!=image-0:tag3"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) // Validate images by name - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:image==image-1"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:image==image-1"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) // Validate images by name - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:image!=image-1"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:image!=image-1"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) // Ensure that constraints can be chained. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{ - "affinity:container!=container-n0-1-id", - "affinity:container!=container-n1-1-id", - }, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:container!=container-n0-1-id", "affinity:container!=container-n1-1-id"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[2]) // Ensure that constraints can be chained. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{ - "affinity:container==container-n0-1-id", - "affinity:container==container-n1-1-id", - }, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:container==container-n0-1-id", "affinity:container==container-n1-1-id"}}), nodes) assert.Error(t, err) //Tests for Soft affinity - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:image==~image-0:tag3"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:image==~image-0:tag3"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:image==~ima~ge-0:tag3"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:image==~ima~ge-0:tag3"}}), nodes) assert.Error(t, err) assert.Len(t, result, 0) - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:image==~image-1:tag3"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:image==~image-1:tag3"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 3) - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:image==~image-*"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:image==~image-*"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:image!=~image-*"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:image!=~image-*"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[2]) - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:image==~/image-\\d*/"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:image==~/image-\\d*/"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) // Not support = any more - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:image=image-0:tag3"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:image=image-0:tag3"}}), nodes) assert.Error(t, err) assert.Len(t, result, 0) // Not support =! any more - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{ - Env: []string{"affinity:image=!image-0:tag3"}, - }}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"affinity:image=!image-0:tag3"}}), nodes) assert.Error(t, err) assert.Len(t, result, 0) diff --git a/scheduler/filter/constraint.go b/scheduler/filter/constraint.go index cb668bdfff..d09192d0b8 100644 --- a/scheduler/filter/constraint.go +++ b/scheduler/filter/constraint.go @@ -19,7 +19,7 @@ func (f *ConstraintFilter) Name() string { // Filter is exported func (f *ConstraintFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { - constraints, err := parseExprs("constraint", config.Env) + constraints, err := parseExprs(config.Constraints()) if err != nil { return nil, err } diff --git a/scheduler/filter/constraint_test.go b/scheduler/filter/constraint_test.go index b7cc241dc4..26a0c440a2 100644 --- a/scheduler/filter/constraint_test.go +++ b/scheduler/filter/constraint_test.go @@ -66,49 +66,49 @@ func TestConstrainteFilter(t *testing.T) { assert.Equal(t, result, nodes) // Set a constraint that cannot be fullfilled and expect an error back. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:does_not_exist==true"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:does_not_exist==true"}}), nodes) assert.Error(t, err) // Set a contraint that can only be filled by a single node. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:name==node1"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:name==node1"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) // This constraint can only be fullfilled by a subset of nodes. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:group==1"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:group==1"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) assert.NotContains(t, result, nodes[2]) // Validate node pinning by id. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:node==node-2-id"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:node==node-2-id"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[2]) // Validate node pinning by name. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:node==node-1-name"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:node==node-1-name"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) // Make sure constraints are evaluated as logical ANDs. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:name==node0", "constraint:group==1"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:name==node0", "constraint:group==1"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) // Check matching - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:region==us"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:region==us"}}), nodes) assert.Error(t, err) assert.Len(t, result, 0) - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:region==us*"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:region==us*"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:region==*us*"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:region==*us*"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) } @@ -122,22 +122,22 @@ func TestConstraintNotExpr(t *testing.T) { ) // Check not (!) expression - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:name!=node0"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:name!=node0"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 3) // Check not does_not_exist. All should be found - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:name!=does_not_exist"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:name!=does_not_exist"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 4) // Check name must not start with n - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:name!=n*"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:name!=n*"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) // Check not with globber pattern - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:region!=us*"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:region!=us*"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) } @@ -151,22 +151,22 @@ func TestConstraintRegExp(t *testing.T) { ) // Check with regular expression /node\d/ matches node{0..2} - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{`constraint:name==/node\d/`}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{`constraint:name==/node\d/`}}), nodes) assert.NoError(t, err) assert.Len(t, result, 3) // Check with regular expression /node\d/ matches node{0..2} - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{`constraint:name==/node[12]/`}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{`constraint:name==/node[12]/`}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) // Check with regular expression ! and regexp /node[12]/ matches node[0] and node[3] - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{`constraint:name!=/node[12]/`}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{`constraint:name!=/node[12]/`}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) // Validate node pinning by ! and regexp. - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:node!=/node-[01]-id/"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:node!=/node-[01]-id/"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) } @@ -187,19 +187,19 @@ func TestFilterRegExpCaseInsensitive(t *testing.T) { } // Case-sensitive, so not match - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{`constraint:name==/abcdef/`}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{`constraint:name==/abcdef/`}}), nodes) assert.Error(t, err) assert.Len(t, result, 0) // Match with case-insensitive - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{`constraint:name==/(?i)abcdef/`}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{`constraint:name==/(?i)abcdef/`}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[3]) assert.Equal(t, result[0].Labels["name"], "aBcDeF") // Test ! filter combined with case insensitive - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{`constraint:name!=/(?i)abc*/`}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{`constraint:name!=/(?i)abc*/`}}), nodes) assert.NoError(t, err) assert.Len(t, result, 3) } @@ -213,17 +213,17 @@ func TestFilterEquals(t *testing.T) { ) // Check == comparison - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:name==node0"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:name==node0"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) // Test == with glob - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:region==us*"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:region==us*"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) // Validate node name with == - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:node==node-1-name"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:node==node-1-name"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) @@ -237,11 +237,11 @@ func TestUnsupportedOperators(t *testing.T) { err error ) - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:name=node0"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:name=node0"}}), nodes) assert.Error(t, err) assert.Len(t, result, 0) - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:name=!node0"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:name=!node0"}}), nodes) assert.Error(t, err) assert.Len(t, result, 0) } @@ -254,26 +254,26 @@ func TestFilterSoftConstraint(t *testing.T) { err error ) - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:node==~node-1-name"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:node==~node-1-name"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{`constraint:name!=~/(?i)abc*/`}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{`constraint:name!=~/(?i)abc*/`}}), nodes) assert.NoError(t, err) assert.Len(t, result, 4) // Check not with globber pattern - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:region!=~us*"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:region!=~us*"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 2) - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:region!=~can*"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:region!=~can*"}}), nodes) assert.NoError(t, err) assert.Len(t, result, 4) // Check matching - result, err = f.Filter(&cluster.ContainerConfig{dockerclient.ContainerConfig{Env: []string{"constraint:region==~us~"}}}, nodes) + result, err = f.Filter(cluster.BuildContainerConfig(&dockerclient.ContainerConfig{Env: []string{"constraint:region==~us~"}}), nodes) assert.Error(t, err) assert.Len(t, result, 0) } diff --git a/scheduler/filter/expr.go b/scheduler/filter/expr.go index 67ea47e604..7cef237510 100644 --- a/scheduler/filter/expr.go +++ b/scheduler/filter/expr.go @@ -25,52 +25,49 @@ type expr struct { isSoft bool } -func parseExprs(key string, env []string) ([]expr, error) { +func parseExprs(env []string) ([]expr, error) { exprs := []expr{} for _, e := range env { - if strings.HasPrefix(e, key+":") { - entry := strings.TrimPrefix(e, key+":") - found := false - for i, op := range OPERATORS { - if strings.Contains(entry, op) { - // split with the op - parts := strings.SplitN(entry, op, 2) + found := false + for i, op := range OPERATORS { + if strings.Contains(e, op) { + // split with the op + parts := strings.SplitN(e, op, 2) - // validate key - // allow alpha-numeric - matched, err := regexp.MatchString(`^(?i)[a-z_][a-z0-9\-_.]+$`, parts[0]) + // validate key + // allow alpha-numeric + matched, err := regexp.MatchString(`^(?i)[a-z_][a-z0-9\-_.]+$`, parts[0]) + if err != nil { + return nil, err + } + if matched == false { + return nil, fmt.Errorf("Key '%s' is invalid", parts[0]) + } + + if len(parts) == 2 { + + // validate value + // allow leading = in case of using == + // allow * for globbing + // allow regexp + matched, err := regexp.MatchString(`^(?i)[=!\/]?(~)?[a-z0-9:\-_\s\.\*/\(\)\?\+\[\]\\\^\$\|]+$`, parts[1]) if err != nil { return nil, err } if matched == false { - return nil, fmt.Errorf("Key '%s' is invalid", parts[0]) + return nil, fmt.Errorf("Value '%s' is invalid", parts[1]) } - - if len(parts) == 2 { - - // validate value - // allow leading = in case of using == - // allow * for globbing - // allow regexp - matched, err := regexp.MatchString(`^(?i)[=!\/]?(~)?[a-z0-9:\-_\s\.\*/\(\)\?\+\[\]\\\^\$\|]+$`, parts[1]) - if err != nil { - return nil, err - } - if matched == false { - return nil, fmt.Errorf("Value '%s' is invalid", parts[1]) - } - exprs = append(exprs, expr{key: strings.ToLower(parts[0]), operator: i, value: strings.TrimLeft(parts[1], "~"), isSoft: isSoft(parts[1])}) - } else { - exprs = append(exprs, expr{key: strings.ToLower(parts[0]), operator: i}) - } - - found = true - break // found an op, move to next entry + exprs = append(exprs, expr{key: strings.ToLower(parts[0]), operator: i, value: strings.TrimLeft(parts[1], "~"), isSoft: isSoft(parts[1])}) + } else { + exprs = append(exprs, expr{key: strings.ToLower(parts[0]), operator: i}) } + + found = true + break // found an op, move to next entry } - if !found { - return nil, fmt.Errorf("One of operator ==, != is expected") - } + } + if !found { + return nil, fmt.Errorf("One of operator ==, != is expected") } } return exprs, nil diff --git a/scheduler/filter/expr_test.go b/scheduler/filter/expr_test.go index b506e4ff44..9ed36fa67e 100644 --- a/scheduler/filter/expr_test.go +++ b/scheduler/filter/expr_test.go @@ -8,43 +8,43 @@ import ( func TestParseExprs(t *testing.T) { // Cannot use the leading digit for key - _, err := parseExprs("constraint", []string{"constraint:1node"}) + _, err := parseExprs([]string{"1node"}) assert.Error(t, err) // Cannot use space in key - _, err = parseExprs("constraint", []string{"constraint:node ==node1"}) + _, err = parseExprs([]string{"node ==node1"}) assert.Error(t, err) // Cannot use * in key - _, err = parseExprs("constraint", []string{"constraint:no*de==node1"}) + _, err = parseExprs([]string{"no*de==node1"}) assert.Error(t, err) // Cannot use $ in key - _, err = parseExprs("constraint", []string{"constraint:no$de==node1"}) + _, err = parseExprs([]string{"no$de==node1"}) assert.Error(t, err) // Allow CAPS in key - _, err = parseExprs("constraint", []string{"constraint:NoDe==node1"}) + _, err = parseExprs([]string{"NoDe==node1"}) assert.NoError(t, err) // Allow dot in key - _, err = parseExprs("constraint", []string{"constraint:no.de==node1"}) + _, err = parseExprs([]string{"no.de==node1"}) assert.NoError(t, err) // Allow leading underscore - _, err = parseExprs("constraint", []string{"constraint:_node==_node1"}) + _, err = parseExprs([]string{"_node==_node1"}) assert.NoError(t, err) // Allow globbing - _, err = parseExprs("constraint", []string{"constraint:node==*node*"}) + _, err = parseExprs([]string{"node==*node*"}) assert.NoError(t, err) // Allow regexp in value - _, err = parseExprs("constraint", []string{"constraint:node==/(?i)^[a-b]+c*(n|b)$/"}) + _, err = parseExprs([]string{"node==/(?i)^[a-b]+c*(n|b)$/"}) assert.NoError(t, err) // Allow space in value - _, err = parseExprs("constraint", []string{"constraint:node==node 1"}) + _, err = parseExprs([]string{"node==node 1"}) assert.NoError(t, err) } diff --git a/test/integration/affinities.bats b/test/integration/affinities.bats index 002c7ad2f8..fb1b7ddf84 100644 --- a/test/integration/affinities.bats +++ b/test/integration/affinities.bats @@ -17,6 +17,10 @@ function teardown() { [ "$status" -eq 0 ] run docker_swarm run --name c3 -e affinity:container!=c1 -d busybox:latest sh [ "$status" -eq 0 ] + run docker_swarm run --name c4 --label 'com.docker.swarm.affinities=["container==c1"]' -d busybox:latest sh + [ "$status" -eq 0 ] + run docker_swarm run --name c5 --label 'com.docker.swarm.affinities=["container\!=c1"]' -d busybox:latest sh + [ "$status" -eq 0 ] run docker_swarm inspect c1 [ "$status" -eq 0 ] @@ -29,6 +33,14 @@ function teardown() { run docker_swarm inspect c3 [ "$status" -eq 0 ] [[ "${output}" != *'"Name": "node-0"'* ]] + + run docker_swarm inspect c4 + [ "$status" -eq 0 ] + [[ "${output}" == *'"Name": "node-0"'* ]] + + run docker_swarm inspect c5 + [ "$status" -eq 0 ] + [[ "${output}" != *'"Name": "node-0"'* ]] } @test "image affinity" { @@ -41,6 +53,10 @@ function teardown() { [ "$status" -eq 0 ] run docker_swarm run --name c2 -e affinity:image!=busybox -d busybox:latest sh [ "$status" -eq 0 ] + run docker_swarm run --name c3 --label 'com.docker.swarm.affinities=["image==busybox"]' -d busybox:latest sh + [ "$status" -eq 0 ] + run docker_swarm run --name c4 --label 'com.docker.swarm.affinities=["image\!=busybox"]' -d busybox:latest sh + [ "$status" -eq 0 ] run docker_swarm inspect c1 [ "$status" -eq 0 ] @@ -49,6 +65,14 @@ function teardown() { run docker_swarm inspect c2 [ "$status" -eq 0 ] [[ "${output}" != *'"Name": "node-0"'* ]] + + run docker_swarm inspect c3 + [ "$status" -eq 0 ] + [[ "${output}" == *'"Name": "node-0"'* ]] + + run docker_swarm inspect c4 + [ "$status" -eq 0 ] + [[ "${output}" != *'"Name": "node-0"'* ]] } @test "label affinity" { @@ -61,6 +85,10 @@ function teardown() { [ "$status" -eq 0 ] run docker_swarm run --name c3 -e affinity:test.label!=true -d busybox:latest sh [ "$status" -eq 0 ] + run docker_swarm run --name c4 --label 'com.docker.swarm.affinities=["test.label==true"]' -d busybox:latest sh + [ "$status" -eq 0 ] + run docker_swarm run --name c5 --label 'com.docker.swarm.affinities=["test.label\!=true"]' -d busybox:latest sh + [ "$status" -eq 0 ] run docker_swarm inspect c1 [ "$status" -eq 0 ] @@ -73,4 +101,12 @@ function teardown() { run docker_swarm inspect c3 [ "$status" -eq 0 ] [[ "${output}" != *'"Name": "node-0"'* ]] + + run docker_swarm inspect c4 + [ "$status" -eq 0 ] + [[ "${output}" == *'"Name": "node-0"'* ]] + + run docker_swarm inspect c5 + [ "$status" -eq 0 ] + [[ "${output}" != *'"Name": "node-0"'* ]] } diff --git a/test/integration/constraints.bats b/test/integration/constraints.bats index 186316bd84..28b70b5d2a 100644 --- a/test/integration/constraints.bats +++ b/test/integration/constraints.bats @@ -17,7 +17,9 @@ function teardown() { [ "$status" -eq 0 ] run docker_swarm run --name c3 -e constraint:node==node-1 -d busybox:latest sh [ "$status" -eq 0 ] - + run docker_swarm run --name c4 --label 'com.docker.swarm.constraints=["node==node-1"]' -d busybox:latest sh + [ "$status" -eq 0 ] + run docker_swarm inspect c1 [ "$status" -eq 0 ] [[ "${output}" == *'"Name": "node-0"'* ]] @@ -29,6 +31,10 @@ function teardown() { run docker_swarm inspect c3 [ "$status" -eq 0 ] [[ "${output}" == *'"Name": "node-1"'* ]] + + run docker_swarm inspect c4 + [ "$status" -eq 0 ] + [[ "${output}" == *'"Name": "node-1"'* ]] } @test "label constraints" { @@ -42,6 +48,8 @@ function teardown() { [ "$status" -eq 0 ] run docker_swarm run --name c3 -e constraint:foo==b -d busybox:latest sh [ "$status" -eq 0 ] + run docker_swarm run --name c4 --label 'com.docker.swarm.constraints=["foo==b"]' -d busybox:latest sh + [ "$status" -eq 0 ] run docker_swarm inspect c1 [ "$status" -eq 0 ] @@ -54,4 +62,8 @@ function teardown() { run docker_swarm inspect c3 [ "$status" -eq 0 ] [[ "${output}" == *'"Name": "node-1"'* ]] + + run docker_swarm inspect c4 + [ "$status" -eq 0 ] + [[ "${output}" == *'"Name": "node-1"'* ]] }