diff --git a/api/handlers.go b/api/handlers.go index 43d44ce3b5..a5e468331c 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -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/cluster.go b/cluster/cluster.go index b6031e48db..d40729c3b8 100644 --- a/cluster/cluster.go +++ b/cluster/cluster.go @@ -9,7 +9,7 @@ import ( // Cluster is exported type Cluster interface { // Create a container - CreateContainer(config *dockerclient.ContainerConfig, name string) (*Container, error) + CreateContainer(config *ContainerConfig, name string) (*Container, error) // Remove a container RemoveContainer(container *Container, force bool) error diff --git a/cluster/config.go b/cluster/config.go new file mode 100644 index 0000000000..762c9e8a1f --- /dev/null +++ b/cluster/config.go @@ -0,0 +1,98 @@ +package cluster + +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/cluster/engine.go b/cluster/engine.go index a9e6eabf7f..6fec790125 100644 --- a/cluster/engine.go +++ b/cluster/engine.go @@ -349,7 +349,7 @@ func (e *Engine) TotalCpus() int64 { } // Create a new container -func (e *Engine) Create(config *dockerclient.ContainerConfig, name string, pullImage bool) (*Container, error) { +func (e *Engine) Create(config *ContainerConfig, name string, pullImage bool) (*Container, error) { var ( err error id string @@ -361,7 +361,7 @@ func (e *Engine) Create(config *dockerclient.ContainerConfig, name string, pullI // nb of CPUs -> real CpuShares newConfig.CpuShares = config.CpuShares * 1024 / e.Cpus - if id, err = client.CreateContainer(&newConfig, name); err != nil { + if id, err = client.CreateContainer(&newConfig.ContainerConfig, name); err != nil { // If the error is other than not found, abort immediately. if err != dockerclient.ErrNotFound || !pullImage { return nil, err @@ -371,7 +371,7 @@ func (e *Engine) Create(config *dockerclient.ContainerConfig, name string, pullI return nil, err } // ...And try agaie. - if id, err = client.CreateContainer(&newConfig, name); err != nil { + if id, err = client.CreateContainer(&newConfig.ContainerConfig, name); err != nil { return nil, err } } diff --git a/cluster/engine_test.go b/cluster/engine_test.go index 48cc6e6ab7..69dac1b152 100644 --- a/cluster/engine_test.go +++ b/cluster/engine_test.go @@ -169,12 +169,12 @@ func TestEngineContainerLookup(t *testing.T) { func TestCreateContainer(t *testing.T) { var ( - config = &dockerclient.ContainerConfig{ + config = &ContainerConfig{dockerclient.ContainerConfig{ Image: "busybox", CpuShares: 1, Cmd: []string{"date"}, Tty: false, - } + }} engine = NewEngine("test", 0) client = mockclient.NewMockClient() ) @@ -186,7 +186,7 @@ func TestCreateContainer(t *testing.T) { assert.NoError(t, engine.connectClient(client)) assert.True(t, engine.isConnected()) - mockConfig := *config + mockConfig := config.ContainerConfig mockConfig.CpuShares = config.CpuShares * 1024 / mockInfo.NCPU // Everything is ok @@ -195,7 +195,7 @@ func TestCreateContainer(t *testing.T) { client.On("CreateContainer", &mockConfig, name).Return(id, nil).Once() client.On("ListContainers", true, false, fmt.Sprintf(`{"id":[%q]}`, id)).Return([]dockerclient.Container{{Id: id}}, nil).Once() client.On("ListImages").Return([]*dockerclient.Image{}, nil).Once() - client.On("InspectContainer", id).Return(&dockerclient.ContainerInfo{Config: config}, nil).Once() + client.On("InspectContainer", id).Return(&dockerclient.ContainerInfo{Config: &config.ContainerConfig}, nil).Once() container, err := engine.Create(config, name, false) assert.Nil(t, err) assert.Equal(t, container.Id, id) @@ -218,7 +218,7 @@ func TestCreateContainer(t *testing.T) { client.On("CreateContainer", &mockConfig, name).Return(id, nil).Once() client.On("ListContainers", true, false, fmt.Sprintf(`{"id":[%q]}`, id)).Return([]dockerclient.Container{{Id: id}}, nil).Once() client.On("ListImages").Return([]*dockerclient.Image{}, nil).Once() - client.On("InspectContainer", id).Return(&dockerclient.ContainerInfo{Config: config}, nil).Once() + client.On("InspectContainer", id).Return(&dockerclient.ContainerInfo{Config: &config.ContainerConfig}, nil).Once() container, err = engine.Create(config, name, true) assert.Nil(t, err) assert.Equal(t, container.Id, id) diff --git a/cluster/swarm/cluster.go b/cluster/swarm/cluster.go index 9ac1b34719..ab6f49a3f3 100644 --- a/cluster/swarm/cluster.go +++ b/cluster/swarm/cluster.go @@ -81,7 +81,7 @@ func (c *Cluster) RegisterEventHandler(h cluster.EventHandler) error { } // CreateContainer aka schedule a brand new container into the cluster. -func (c *Cluster) CreateContainer(config *dockerclient.ContainerConfig, name string) (*cluster.Container, error) { +func (c *Cluster) CreateContainer(config *cluster.ContainerConfig, name string) (*cluster.Container, error) { c.scheduler.Lock() defer c.scheduler.Unlock() @@ -376,7 +376,7 @@ func (c *Cluster) Info() [][2]string { // RANDOMENGINE returns a random engine. func (c *Cluster) RANDOMENGINE() (*cluster.Engine, error) { - n, err := c.scheduler.SelectNodeForContainer(c.listNodes(), &dockerclient.ContainerConfig{}) + n, err := c.scheduler.SelectNodeForContainer(c.listNodes(), &cluster.ContainerConfig{}) if err != nil { return nil, err } diff --git a/scheduler/filter/affinity.go b/scheduler/filter/affinity.go index 6dffe26e4a..2f50b48415 100644 --- a/scheduler/filter/affinity.go +++ b/scheduler/filter/affinity.go @@ -5,8 +5,8 @@ import ( "strings" log "github.com/Sirupsen/logrus" + "github.com/docker/swarm/cluster" "github.com/docker/swarm/scheduler/node" - "github.com/samalba/dockerclient" ) // AffinityFilter selects only nodes based on other containers on the node. @@ -19,8 +19,8 @@ func (f *AffinityFilter) Name() string { } // Filter is exported -func (f *AffinityFilter) Filter(config *dockerclient.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { - affinities, err := parseExprs("affinity", config.Env) +func (f *AffinityFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { + 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 b05be60b59..91a21b90c9 100644 --- a/scheduler/filter/affinity_test.go +++ b/scheduler/filter/affinity_test.go @@ -62,187 +62,133 @@ func TestAffinityFilter(t *testing.T) { ) // Without constraints we should get the unfiltered list of nodes back. - result, err = f.Filter(&dockerclient.ContainerConfig{}, nodes) + result, err = f.Filter(&cluster.ContainerConfig{}, nodes) assert.NoError(t, err) assert.Equal(t, result, nodes) // Set a constraint that cannot be fulfilled and expect an error back. - result, err = f.Filter(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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 b5cf0a778f..d09192d0b8 100644 --- a/scheduler/filter/constraint.go +++ b/scheduler/filter/constraint.go @@ -4,8 +4,8 @@ import ( "fmt" log "github.com/Sirupsen/logrus" + "github.com/docker/swarm/cluster" "github.com/docker/swarm/scheduler/node" - "github.com/samalba/dockerclient" ) // ConstraintFilter selects only nodes that match certain labels. @@ -18,8 +18,8 @@ func (f *ConstraintFilter) Name() string { } // Filter is exported -func (f *ConstraintFilter) Filter(config *dockerclient.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { - constraints, err := parseExprs("constraint", config.Env) +func (f *ConstraintFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { + 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 cc14d24924..26a0c440a2 100644 --- a/scheduler/filter/constraint_test.go +++ b/scheduler/filter/constraint_test.go @@ -3,6 +3,7 @@ package filter import ( "testing" + "github.com/docker/swarm/cluster" "github.com/docker/swarm/scheduler/node" "github.com/samalba/dockerclient" "github.com/stretchr/testify/assert" @@ -60,54 +61,54 @@ func TestConstrainteFilter(t *testing.T) { ) // Without constraints we should get the unfiltered list of nodes back. - result, err = f.Filter(&dockerclient.ContainerConfig{}, nodes) + result, err = f.Filter(&cluster.ContainerConfig{}, nodes) assert.NoError(t, err) assert.Equal(t, result, nodes) // Set a constraint that cannot be fullfilled and expect an error back. - result, err = f.Filter(&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(&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(&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(&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(&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(&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(&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(&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(&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) } @@ -121,22 +122,22 @@ func TestConstraintNotExpr(t *testing.T) { ) // Check not (!) expression - result, err = f.Filter(&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(&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(&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(&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) } @@ -150,22 +151,22 @@ func TestConstraintRegExp(t *testing.T) { ) // Check with regular expression /node\d/ matches node{0..2} - result, err = f.Filter(&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(&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(&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(&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) } @@ -186,19 +187,19 @@ func TestFilterRegExpCaseInsensitive(t *testing.T) { } // Case-sensitive, so not match - result, err = f.Filter(&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(&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(&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) } @@ -212,17 +213,17 @@ func TestFilterEquals(t *testing.T) { ) // Check == comparison - result, err = f.Filter(&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(&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(&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]) @@ -236,11 +237,11 @@ func TestUnsupportedOperators(t *testing.T) { err error ) - result, err = f.Filter(&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(&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) } @@ -253,26 +254,26 @@ func TestFilterSoftConstraint(t *testing.T) { err error ) - result, err = f.Filter(&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(&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(&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(&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(&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/dependency.go b/scheduler/filter/dependency.go index f94183a367..2e27b430b4 100644 --- a/scheduler/filter/dependency.go +++ b/scheduler/filter/dependency.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" + "github.com/docker/swarm/cluster" "github.com/docker/swarm/scheduler/node" - "github.com/samalba/dockerclient" ) // DependencyFilter co-schedules dependent containers on the same node. @@ -18,7 +18,7 @@ func (f *DependencyFilter) Name() string { } // Filter is exported -func (f *DependencyFilter) Filter(config *dockerclient.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { +func (f *DependencyFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { if len(nodes) == 0 { return nodes, nil } @@ -52,7 +52,7 @@ func (f *DependencyFilter) Filter(config *dockerclient.ContainerConfig, nodes [] } // Get a string representation of the dependencies found in the container config. -func (f *DependencyFilter) String(config *dockerclient.ContainerConfig) string { +func (f *DependencyFilter) String(config *cluster.ContainerConfig) string { dependencies := []string{} for _, volume := range config.HostConfig.VolumesFrom { dependencies = append(dependencies, fmt.Sprintf("--volumes-from=%s", volume)) diff --git a/scheduler/filter/dependency_test.go b/scheduler/filter/dependency_test.go index b0bbeddb0f..8c84939ce4 100644 --- a/scheduler/filter/dependency_test.go +++ b/scheduler/filter/dependency_test.go @@ -36,46 +36,46 @@ func TestDependencyFilterSimple(t *testing.T) { } result []*node.Node err error - config *dockerclient.ContainerConfig + config *cluster.ContainerConfig ) // No dependencies - make sure we don't filter anything out. - config = &dockerclient.ContainerConfig{} + config = &cluster.ContainerConfig{} result, err = f.Filter(config, nodes) assert.NoError(t, err) assert.Equal(t, result, nodes) // volumes-from. - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c0"}, - }} + }}} result, err = f.Filter(config, nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) // link. - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ Links: []string{"c1:foobar"}, - }} + }}} result, err = f.Filter(config, nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[1]) // net. - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ NetworkMode: "container:c2", - }} + }}} result, err = f.Filter(config, nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[2]) // net not prefixed by "container:" should be ignored. - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ NetworkMode: "bridge", - }} + }}} result, err = f.Filter(config, nodes) assert.NoError(t, err) assert.Equal(t, result, nodes) @@ -113,40 +113,40 @@ func TestDependencyFilterMulti(t *testing.T) { } result []*node.Node err error - config *dockerclient.ContainerConfig + config *cluster.ContainerConfig ) // Depend on c0 which is on nodes[0] - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c0"}, - }} + }}} result, err = f.Filter(config, nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) // Depend on c1 which is on nodes[0] - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c1"}, - }} + }}} result, err = f.Filter(config, nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) // Depend on c0 AND c1 which are both on nodes[0] - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c0", "c1"}, - }} + }}} result, err = f.Filter(config, nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) // Depend on c0 AND c2 which are on different nodes. - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c0", "c2"}, - }} + }}} result, err = f.Filter(config, nodes) assert.Error(t, err) } @@ -183,30 +183,30 @@ func TestDependencyFilterChaining(t *testing.T) { } result []*node.Node err error - config *dockerclient.ContainerConfig + config *cluster.ContainerConfig ) // Different dependencies on c0 and c1 - config = &dockerclient.ContainerConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{ HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c0"}, Links: []string{"c1"}, NetworkMode: "container:c1", }, - } + }} result, err = f.Filter(config, nodes) assert.NoError(t, err) assert.Len(t, result, 1) assert.Equal(t, result[0], nodes[0]) // Different dependencies on c0 and c2 - config = &dockerclient.ContainerConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{ HostConfig: dockerclient.HostConfig{ VolumesFrom: []string{"c0"}, Links: []string{"c2"}, NetworkMode: "container:c1", }, - } + }} result, err = f.Filter(config, nodes) assert.Error(t, err) } 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/scheduler/filter/filter.go b/scheduler/filter/filter.go index df2afbc1e2..10657fc393 100644 --- a/scheduler/filter/filter.go +++ b/scheduler/filter/filter.go @@ -4,8 +4,8 @@ import ( "errors" log "github.com/Sirupsen/logrus" + "github.com/docker/swarm/cluster" "github.com/docker/swarm/scheduler/node" - "github.com/samalba/dockerclient" ) // Filter is exported @@ -13,7 +13,7 @@ type Filter interface { Name() string // Return a subset of nodes that were accepted by the filtering policy. - Filter(*dockerclient.ContainerConfig, []*node.Node) ([]*node.Node, error) + Filter(*cluster.ContainerConfig, []*node.Node) ([]*node.Node, error) } var ( @@ -54,7 +54,7 @@ func New(names []string) ([]Filter, error) { } // ApplyFilters applies a set of filters in batch. -func ApplyFilters(filters []Filter, config *dockerclient.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { +func ApplyFilters(filters []Filter, config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { var err error for _, filter := range filters { diff --git a/scheduler/filter/health.go b/scheduler/filter/health.go index b0148107d0..97966c7e38 100644 --- a/scheduler/filter/health.go +++ b/scheduler/filter/health.go @@ -3,8 +3,8 @@ package filter import ( "errors" + "github.com/docker/swarm/cluster" "github.com/docker/swarm/scheduler/node" - "github.com/samalba/dockerclient" ) var ( @@ -22,7 +22,7 @@ func (f *HealthFilter) Name() string { } // Filter is exported -func (f *HealthFilter) Filter(_ *dockerclient.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { +func (f *HealthFilter) Filter(_ *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { result := []*node.Node{} for _, node := range nodes { if node.IsHealthy { diff --git a/scheduler/filter/port.go b/scheduler/filter/port.go index ef1e319757..fe9a11f3be 100644 --- a/scheduler/filter/port.go +++ b/scheduler/filter/port.go @@ -3,6 +3,7 @@ package filter import ( "fmt" + "github.com/docker/swarm/cluster" "github.com/docker/swarm/scheduler/node" "github.com/samalba/dockerclient" ) @@ -19,7 +20,7 @@ func (p *PortFilter) Name() string { } // Filter is exported -func (p *PortFilter) Filter(config *dockerclient.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { +func (p *PortFilter) Filter(config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { if config.HostConfig.NetworkMode == "host" { return p.filterHost(config, nodes) } @@ -27,7 +28,7 @@ func (p *PortFilter) Filter(config *dockerclient.ContainerConfig, nodes []*node. return p.filterBridge(config, nodes) } -func (p *PortFilter) filterHost(config *dockerclient.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { +func (p *PortFilter) filterHost(config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { for port := range config.ExposedPorts { candidates := []*node.Node{} for _, node := range nodes { @@ -43,7 +44,7 @@ func (p *PortFilter) filterHost(config *dockerclient.ContainerConfig, nodes []*n return nodes, nil } -func (p *PortFilter) filterBridge(config *dockerclient.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { +func (p *PortFilter) filterBridge(config *cluster.ContainerConfig, nodes []*node.Node) ([]*node.Node, error) { for _, port := range config.HostConfig.PortBindings { for _, binding := range port { candidates := []*node.Node{} diff --git a/scheduler/filter/port_test.go b/scheduler/filter/port_test.go index 41d5e43807..bb95c5b227 100644 --- a/scheduler/filter/port_test.go +++ b/scheduler/filter/port_test.go @@ -46,18 +46,18 @@ func TestPortFilterNoConflicts(t *testing.T) { ) // Request no ports. - config := &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config := &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ PortBindings: map[string][]dockerclient.PortBinding{}, - }} + }}} // Make sure we don't filter anything out. result, err = p.Filter(config, nodes) assert.NoError(t, err) assert.Equal(t, result, nodes) // Request port 80. - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ PortBindings: makeBinding("", "80"), - }} + }}} // Since there are no other containers in the cluster, this shouldn't // filter anything either. @@ -104,9 +104,9 @@ func TestPortFilterSimple(t *testing.T) { assert.NoError(t, nodes[0].AddContainer(container)) // Request port 80. - config := &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config := &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ PortBindings: makeBinding("", "80"), - }} + }}} // nodes[0] should be excluded since port 80 is taken away. result, err = p.Filter(config, nodes) @@ -143,9 +143,9 @@ func TestPortFilterDifferentInterfaces(t *testing.T) { assert.NoError(t, nodes[0].AddContainer(container)) // Request port 80 for the local interface. - config := &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config := &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ PortBindings: makeBinding("127.0.0.1", "80"), - }} + }}} // nodes[0] should be excluded since port 80 is taken away for every // interface. @@ -158,9 +158,9 @@ func TestPortFilterDifferentInterfaces(t *testing.T) { container = &cluster.Container{Container: dockerclient.Container{Id: "c1"}, Info: dockerclient.ContainerInfo{HostConfig: &dockerclient.HostConfig{PortBindings: makeBinding("127.0.0.1", "4242")}}} assert.NoError(t, nodes[1].AddContainer(container)) // Request port 4242 on the same interface. - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ PortBindings: makeBinding("127.0.0.1", "4242"), - }} + }}} // nodes[1] should be excluded since port 4242 is already taken on that // interface. result, err = p.Filter(config, nodes) @@ -168,27 +168,27 @@ func TestPortFilterDifferentInterfaces(t *testing.T) { assert.NotContains(t, result, nodes[1]) // Request port 4242 on every interface. - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ PortBindings: makeBinding("0.0.0.0", "4242"), - }} + }}} // nodes[1] should still be excluded since the port is not available on the same interface. result, err = p.Filter(config, nodes) assert.NoError(t, err) assert.NotContains(t, result, nodes[1]) // Request port 4242 on every interface using an alternative syntax. - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ PortBindings: makeBinding("", "4242"), - }} + }}} // nodes[1] should still be excluded since the port is not available on the same interface. result, err = p.Filter(config, nodes) assert.NoError(t, err) assert.NotContains(t, result, nodes[1]) // Finally, request port 4242 on a different interface. - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ PortBindings: makeBinding("192.168.1.1", "4242"), - }} + }}} // nodes[1] should be included this time since the port is available on the // other interface. result, err = p.Filter(config, nodes) @@ -257,9 +257,9 @@ func TestPortFilterRandomAssignment(t *testing.T) { assert.NoError(t, nodes[0].AddContainer(container)) // Request port 80. - config := &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config := &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ PortBindings: makeBinding("", "80"), - }} + }}} // Since port "80" has been mapped to "1234", we should be able to request "80". result, err = p.Filter(config, nodes) @@ -267,9 +267,9 @@ func TestPortFilterRandomAssignment(t *testing.T) { assert.Equal(t, result, nodes) // However, we should not be able to request "1234" since it has been used for a random assignment. - config = &dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ + config = &cluster.ContainerConfig{dockerclient.ContainerConfig{HostConfig: dockerclient.HostConfig{ PortBindings: makeBinding("", "1234"), - }} + }}} result, err = p.Filter(config, nodes) assert.NoError(t, err) assert.NotContains(t, result, nodes[0]) @@ -315,12 +315,12 @@ func TestPortFilterForHostMode(t *testing.T) { assert.NoError(t, nodes[0].AddContainer(container)) // Request port 80 in the host mode - config := &dockerclient.ContainerConfig{ + config := &cluster.ContainerConfig{dockerclient.ContainerConfig{ ExposedPorts: map[string]struct{}{"80": {}}, HostConfig: dockerclient.HostConfig{ NetworkMode: "host", }, - } + }} // nodes[0] should be excluded since port 80 is taken away result, err = p.Filter(config, nodes) diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go index c85763ecda..c103f1a700 100644 --- a/scheduler/scheduler.go +++ b/scheduler/scheduler.go @@ -4,10 +4,10 @@ import ( "strings" "sync" + "github.com/docker/swarm/cluster" "github.com/docker/swarm/scheduler/filter" "github.com/docker/swarm/scheduler/node" "github.com/docker/swarm/scheduler/strategy" - "github.com/samalba/dockerclient" ) // Scheduler is exported @@ -27,7 +27,7 @@ func New(strategy strategy.PlacementStrategy, filters []filter.Filter) *Schedule } // SelectNodeForContainer will find a nice home for our container. -func (s *Scheduler) SelectNodeForContainer(nodes []*node.Node, config *dockerclient.ContainerConfig) (*node.Node, error) { +func (s *Scheduler) SelectNodeForContainer(nodes []*node.Node, config *cluster.ContainerConfig) (*node.Node, error) { accepted, err := filter.ApplyFilters(s.filters, config, nodes) if err != nil { return nil, err diff --git a/scheduler/strategy/binpack.go b/scheduler/strategy/binpack.go index c168b564ac..195a2e7a95 100644 --- a/scheduler/strategy/binpack.go +++ b/scheduler/strategy/binpack.go @@ -3,8 +3,8 @@ package strategy import ( "sort" + "github.com/docker/swarm/cluster" "github.com/docker/swarm/scheduler/node" - "github.com/samalba/dockerclient" ) // BinpackPlacementStrategy is exported @@ -22,7 +22,7 @@ func (p *BinpackPlacementStrategy) Name() string { } // PlaceContainer is exported -func (p *BinpackPlacementStrategy) PlaceContainer(config *dockerclient.ContainerConfig, nodes []*node.Node) (*node.Node, error) { +func (p *BinpackPlacementStrategy) PlaceContainer(config *cluster.ContainerConfig, nodes []*node.Node) (*node.Node, error) { weightedNodes, err := weighNodes(config, nodes) if err != nil { return nil, err diff --git a/scheduler/strategy/binpack_test.go b/scheduler/strategy/binpack_test.go index 78df07e249..f077d697c0 100644 --- a/scheduler/strategy/binpack_test.go +++ b/scheduler/strategy/binpack_test.go @@ -22,12 +22,12 @@ func createNode(ID string, memory int64, cpus int64) *node.Node { } } -func createConfig(memory int64, cpus int64) *dockerclient.ContainerConfig { - return &dockerclient.ContainerConfig{Memory: memory * 1024 * 1024 * 1024, CpuShares: cpus} +func createConfig(memory int64, cpus int64) *cluster.ContainerConfig { + return &cluster.ContainerConfig{dockerclient.ContainerConfig{Memory: memory * 1024 * 1024 * 1024, CpuShares: cpus}} } -func createContainer(ID string, config *dockerclient.ContainerConfig) *cluster.Container { - return &cluster.Container{Container: dockerclient.Container{Id: ID}, Info: dockerclient.ContainerInfo{Config: config}} +func createContainer(ID string, config *cluster.ContainerConfig) *cluster.Container { + return &cluster.Container{Container: dockerclient.Container{Id: ID}, Info: dockerclient.ContainerInfo{Config: &config.ContainerConfig}} } func TestPlaceEqualWeight(t *testing.T) { diff --git a/scheduler/strategy/random.go b/scheduler/strategy/random.go index bc5a240b75..c7a08d3c51 100644 --- a/scheduler/strategy/random.go +++ b/scheduler/strategy/random.go @@ -5,8 +5,8 @@ import ( "math/rand" "time" + "github.com/docker/swarm/cluster" "github.com/docker/swarm/scheduler/node" - "github.com/samalba/dockerclient" ) // RandomPlacementStrategy randomly places the container into the cluster. @@ -24,7 +24,7 @@ func (p *RandomPlacementStrategy) Name() string { } // PlaceContainer is exported -func (p *RandomPlacementStrategy) PlaceContainer(config *dockerclient.ContainerConfig, nodes []*node.Node) (*node.Node, error) { +func (p *RandomPlacementStrategy) PlaceContainer(config *cluster.ContainerConfig, nodes []*node.Node) (*node.Node, error) { if size := len(nodes); size > 0 { return nodes[rand.Intn(size)], nil } diff --git a/scheduler/strategy/spread.go b/scheduler/strategy/spread.go index 5702c97f12..2b3e7f0168 100644 --- a/scheduler/strategy/spread.go +++ b/scheduler/strategy/spread.go @@ -3,8 +3,8 @@ package strategy import ( "sort" + "github.com/docker/swarm/cluster" "github.com/docker/swarm/scheduler/node" - "github.com/samalba/dockerclient" ) // SpreadPlacementStrategy is exported @@ -22,7 +22,7 @@ func (p *SpreadPlacementStrategy) Name() string { } // PlaceContainer is exported -func (p *SpreadPlacementStrategy) PlaceContainer(config *dockerclient.ContainerConfig, nodes []*node.Node) (*node.Node, error) { +func (p *SpreadPlacementStrategy) PlaceContainer(config *cluster.ContainerConfig, nodes []*node.Node) (*node.Node, error) { weightedNodes, err := weighNodes(config, nodes) if err != nil { return nil, err diff --git a/scheduler/strategy/strategy.go b/scheduler/strategy/strategy.go index acf826aca5..d5caf67111 100644 --- a/scheduler/strategy/strategy.go +++ b/scheduler/strategy/strategy.go @@ -4,8 +4,8 @@ import ( "errors" log "github.com/Sirupsen/logrus" + "github.com/docker/swarm/cluster" "github.com/docker/swarm/scheduler/node" - "github.com/samalba/dockerclient" ) // PlacementStrategy is exported @@ -15,7 +15,7 @@ type PlacementStrategy interface { Initialize() error // Given a container configuration and a set of nodes, select the target // node where the container should be scheduled. - PlaceContainer(config *dockerclient.ContainerConfig, nodes []*node.Node) (*node.Node, error) + PlaceContainer(config *cluster.ContainerConfig, nodes []*node.Node) (*node.Node, error) } var ( diff --git a/scheduler/strategy/weighted_node.go b/scheduler/strategy/weighted_node.go index d08d9a9ed1..d2cd42aa97 100644 --- a/scheduler/strategy/weighted_node.go +++ b/scheduler/strategy/weighted_node.go @@ -1,8 +1,8 @@ package strategy import ( + "github.com/docker/swarm/cluster" "github.com/docker/swarm/scheduler/node" - "github.com/samalba/dockerclient" ) // WeightedNode represents a node in the cluster with a given weight, typically used for sorting @@ -32,7 +32,7 @@ func (n weightedNodeList) Less(i, j int) bool { return ip.Weight < jp.Weight } -func weighNodes(config *dockerclient.ContainerConfig, nodes []*node.Node) (weightedNodeList, error) { +func weighNodes(config *cluster.ContainerConfig, nodes []*node.Node) (weightedNodeList, error) { weightedNodes := weightedNodeList{} for _, node := range nodes { diff --git a/state/state.go b/state/state.go index c25cf5f173..9d613accfa 100644 --- a/state/state.go +++ b/state/state.go @@ -1,12 +1,10 @@ package state -import ( - "github.com/samalba/dockerclient" -) +import "github.com/docker/swarm/cluster" // RequestedState is exported type RequestedState struct { ID string Name string - Config *dockerclient.ContainerConfig + Config *cluster.ContainerConfig } 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"'* ]] }