store constaints and affinities in labels

Signed-off-by: Victor Vieux <victorvieux@gmail.com>
This commit is contained in:
Victor Vieux 2015-04-25 01:47:00 -07:00
parent 6eceffacf9
commit f53b5df834
11 changed files with 293 additions and 160 deletions

View File

@ -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

View File

@ -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")
}

52
cluster/config_test.go Normal file
View File

@ -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)
}

View File

@ -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
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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"'* ]]
}

View File

@ -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"'* ]]
}