diff --git a/.gitignore b/.gitignore index acf599143a..438f4251c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -swarm \ No newline at end of file +swarm diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index d4066de1e0..e516876c98 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,6 +1,6 @@ { "ImportPath": "github.com/docker/swarm", - "GoVersion": "go1.3.1", + "GoVersion": "go1.3.3", "Packages": [ "./..." ], @@ -40,6 +40,11 @@ "Comment": "v1.4.1-3245-g443437f", "Rev": "443437f5ea04da9d62bf3e05d7951f7d30e77d96" }, + { + "ImportPath": "github.com/docker/docker/pkg/stringid", + "Comment": "v1.4.1-3245-g443437f", + "Rev": "443437f5ea04da9d62bf3e05d7951f7d30e77d96" + }, { "ImportPath": "github.com/docker/docker/pkg/units", "Comment": "v1.4.1-3245-g443437f", diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/stringid/README.md b/Godeps/_workspace/src/github.com/docker/docker/pkg/stringid/README.md new file mode 100644 index 0000000000..37a5098fd9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/stringid/README.md @@ -0,0 +1 @@ +This package provides helper functions for dealing with string identifiers diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/stringid/stringid.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/stringid/stringid.go new file mode 100644 index 0000000000..bf39df9b73 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/stringid/stringid.go @@ -0,0 +1,38 @@ +package stringid + +import ( + "crypto/rand" + "encoding/hex" + "io" + "strconv" +) + +// TruncateID returns a shorthand version of a string identifier for convenience. +// A collision with other shorthands is very unlikely, but possible. +// In case of a collision a lookup with TruncIndex.Get() will fail, and the caller +// will need to use a langer prefix, or the full-length Id. +func TruncateID(id string) string { + shortLen := 12 + if len(id) < shortLen { + shortLen = len(id) + } + return id[:shortLen] +} + +// GenerateRandomID returns an unique id +func GenerateRandomID() string { + for { + id := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, id); err != nil { + panic(err) // This shouldn't happen + } + value := hex.EncodeToString(id) + // if we try to parse the truncated for as an int and we don't have + // an error then the value is all numberic and causes issues when + // used as a hostname. ref #3869 + if _, err := strconv.ParseInt(TruncateID(value), 10, 64); err == nil { + continue + } + return value + } +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/stringid/stringid_test.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/stringid/stringid_test.go new file mode 100644 index 0000000000..21f8f8a2fb --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/stringid/stringid_test.go @@ -0,0 +1,35 @@ +package stringid + +import "testing" + +func TestGenerateRandomID(t *testing.T) { + id := GenerateRandomID() + + if len(id) != 64 { + t.Fatalf("Id returned is incorrect: %s", id) + } +} + +func TestShortenId(t *testing.T) { + id := GenerateRandomID() + truncID := TruncateID(id) + if len(truncID) != 12 { + t.Fatalf("Id returned is incorrect: truncate on %s returned %s", id, truncID) + } +} + +func TestShortenIdEmpty(t *testing.T) { + id := "" + truncID := TruncateID(id) + if len(truncID) > len(id) { + t.Fatalf("Id returned is incorrect: truncate on %s returned %s", id, truncID) + } +} + +func TestShortenIdInvalid(t *testing.T) { + id := "1234" + truncID := TruncateID(id) + if len(truncID) != len(id) { + t.Fatalf("Id returned is incorrect: truncate on %s returned %s", id, truncID) + } +} diff --git a/api/handlers.go b/api/handlers.go index 5f3996637d..6eac681d04 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -236,6 +236,11 @@ func getContainersJSON(c *context, w http.ResponseWriter, r *http.Request) { tmp.Status = "Pending" } + // Overwrite labels with the ones we have in the config. + // This ensures that we can freely manipulate them in the codebase and + // they will be properly exported back (for instance Swarm IDs). + tmp.Labels = container.Config.Labels + // TODO remove the Node Name in the name when we have a good solution tmp.Names = make([]string, len(container.Names)) for i, name := range container.Names { @@ -511,12 +516,17 @@ func ping(c *context, w http.ResponseWriter, r *http.Request) { // Proxy a request to the right node func proxyContainer(c *context, w http.ResponseWriter, r *http.Request) { - container, err := getContainerFromVars(c, mux.Vars(r)) + name, container, err := getContainerFromVars(c, mux.Vars(r)) if err != nil { httpError(w, err.Error(), http.StatusNotFound) return } + // Set the full container ID in the proxied URL path. + if name != "" { + r.URL.Path = strings.Replace(r.URL.Path, name, container.Id, 1) + } + if err := proxy(c.tlsConfig, container.Engine.Addr, w, r); err != nil { httpError(w, err.Error(), http.StatusInternalServerError) } @@ -612,11 +622,15 @@ func postCommit(c *context, w http.ResponseWriter, r *http.Request) { vars["name"] = r.Form.Get("container") // get container - container, err := getContainerFromVars(c, vars) + name, container, err := getContainerFromVars(c, vars) if err != nil { httpError(w, err.Error(), http.StatusNotFound) return } + // Set the full container ID in the proxied URL path. + if name != "" { + r.URL.RawQuery = strings.Replace(r.URL.RawQuery, name, container.Id, 1) + } cb := func(resp *http.Response) { if resp.StatusCode == http.StatusCreated { @@ -632,7 +646,7 @@ func postCommit(c *context, w http.ResponseWriter, r *http.Request) { // POST /containers/{name:.*}/rename func postRenameContainer(c *context, w http.ResponseWriter, r *http.Request) { - container, err := getContainerFromVars(c, mux.Vars(r)) + _, container, err := getContainerFromVars(c, mux.Vars(r)) if err != nil { httpError(w, err.Error(), http.StatusNotFound) return @@ -655,11 +669,15 @@ func postRenameContainer(c *context, w http.ResponseWriter, r *http.Request) { // Proxy a hijack request to the right node func proxyHijack(c *context, w http.ResponseWriter, r *http.Request) { - container, err := getContainerFromVars(c, mux.Vars(r)) + name, container, err := getContainerFromVars(c, mux.Vars(r)) if err != nil { httpError(w, err.Error(), http.StatusNotFound) return } + // Set the full container ID in the proxied URL path. + if name != "" { + r.URL.Path = strings.Replace(r.URL.Path, name, container.Id, 1) + } if err := hijack(c.tlsConfig, container.Engine.Addr, w, r); err != nil { httpError(w, err.Error(), http.StatusInternalServerError) diff --git a/api/utils.go b/api/utils.go index d66a6a8d50..8041616c80 100644 --- a/api/utils.go +++ b/api/utils.go @@ -26,25 +26,24 @@ func newClientAndScheme(tlsConfig *tls.Config) (*http.Client, string) { return &http.Client{}, "http" } -func getContainerFromVars(c *context, vars map[string]string) (*cluster.Container, error) { +func getContainerFromVars(c *context, vars map[string]string) (string, *cluster.Container, error) { if name, ok := vars["name"]; ok { if container := c.cluster.Container(name); container != nil { - return container, nil + return name, container, nil } - return nil, fmt.Errorf("No such container: %s", name) - + return name, nil, fmt.Errorf("No such container: %s", name) } if ID, ok := vars["execid"]; ok { for _, container := range c.cluster.Containers() { for _, execID := range container.Info.ExecIDs { if ID == execID { - return container, nil + return "", container, nil } } } - return nil, fmt.Errorf("Exec %s not found", ID) + return "", nil, fmt.Errorf("Exec %s not found", ID) } - return nil, errors.New("Not found") + return "", nil, errors.New("Not found") } // from https://github.com/golang/go/blob/master/src/net/http/httputil/reverseproxy.go#L82 diff --git a/cluster/config.go b/cluster/config.go index 149c8ce387..c0381dd261 100644 --- a/cluster/config.go +++ b/cluster/config.go @@ -87,6 +87,17 @@ func (c *ContainerConfig) extractExprs(key string) []string { return exprs } +// SwarmID extracts the Swarm ID from the Config. +// May return an empty string if not set. +func (c *ContainerConfig) SwarmID() string { + return c.Labels[namespace+".id"] +} + +// SetSwarmID sets or overrides the Swarm ID in the Config. +func (c *ContainerConfig) SetSwarmID(id string) { + c.Labels[namespace+".id"] = id +} + // Affinities returns all the affinities from the ContainerConfig func (c *ContainerConfig) Affinities() []string { return c.extractExprs("affinities") diff --git a/cluster/config_test.go b/cluster/config_test.go index 10fd62b0c9..9348deae26 100644 --- a/cluster/config_test.go +++ b/cluster/config_test.go @@ -29,6 +29,18 @@ func TestBuildContainerConfig(t *testing.T) { assert.Len(t, config.Labels, 2) } +func TestSwarmID(t *testing.T) { + // Getter / Setter + config := BuildContainerConfig(dockerclient.ContainerConfig{}) + assert.Empty(t, config.SwarmID()) + config.SetSwarmID("foo") + assert.Equal(t, config.SwarmID(), "foo") + + // Retrieve an existing ID. + config = BuildContainerConfig(dockerclient.ContainerConfig{Labels: map[string]string{namespace + ".id": "test"}}) + assert.Equal(t, config.SwarmID(), "test") +} + func TestConstraints(t *testing.T) { config := BuildContainerConfig(dockerclient.ContainerConfig{}) assert.Empty(t, config.Constraints()) diff --git a/cluster/engine.go b/cluster/engine.go index 7235f23ff7..e2d2aac943 100644 --- a/cluster/engine.go +++ b/cluster/engine.go @@ -463,11 +463,16 @@ func (e *Engine) Container(IDOrName string) *Container { } for _, container := range e.Containers() { - // Match ID prefix. + // Match Container ID prefix. if strings.HasPrefix(container.Id, IDOrName) { return container } + // Match Swarm ID prefix. + if strings.HasPrefix(container.Config.SwarmID(), IDOrName) { + return container + } + // Match name, /name or engine/name. for _, name := range container.Names { if name == IDOrName || name == "/"+IDOrName || container.Engine.ID+name == IDOrName || container.Engine.Name+name == IDOrName { diff --git a/cluster/swarm/cluster.go b/cluster/swarm/cluster.go index 5f4f2254dc..40db12b139 100644 --- a/cluster/swarm/cluster.go +++ b/cluster/swarm/cluster.go @@ -9,6 +9,7 @@ import ( "sync" log "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/units" "github.com/docker/swarm/cluster" "github.com/docker/swarm/discovery" @@ -80,16 +81,29 @@ func (c *Cluster) RegisterEventHandler(h cluster.EventHandler) error { return nil } +// Generate a globally (across the cluster) unique ID. +func (c *Cluster) generateUniqueID() string { + for { + id := stringid.GenerateRandomID() + if c.Container(id) == nil { + return id + } + } +} + // CreateContainer aka schedule a brand new container into the cluster. func (c *Cluster) CreateContainer(config *cluster.ContainerConfig, name string) (*cluster.Container, error) { c.scheduler.Lock() defer c.scheduler.Unlock() - // check new name whether avaliable + // Ensure the name is avaliable if cID := c.getIDFromName(name); cID != "" { return nil, fmt.Errorf("Conflict, The name %s is already assigned to %s. You have to delete (or rename) that container to be able to assign %s to a container again.", name, cID, name) } + // Associate a Swarm ID to the container we are creating. + config.SetSwarmID(c.generateUniqueID()) + n, err := c.scheduler.SelectNodeForContainer(c.listNodes(), config) if err != nil { return nil, err diff --git a/cluster/swarm/cluster_test.go b/cluster/swarm/cluster_test.go index 3078e7b22c..aca6828e4c 100644 --- a/cluster/swarm/cluster_test.go +++ b/cluster/swarm/cluster_test.go @@ -8,13 +8,14 @@ import ( "github.com/stretchr/testify/assert" ) -func createEngine(t *testing.T, ID string, containers ...dockerclient.Container) *cluster.Engine { +func createEngine(t *testing.T, ID string, containers ...*cluster.Container) *cluster.Engine { engine := cluster.NewEngine(ID, 0) engine.Name = ID engine.ID = ID for _, container := range containers { - engine.AddContainer(&cluster.Container{Container: container, Engine: engine}) + container.Engine = engine + engine.AddContainer(container) } return engine @@ -24,9 +25,16 @@ func TestContainerLookup(t *testing.T) { c := &Cluster{ engines: make(map[string]*cluster.Engine), } - container := dockerclient.Container{ - Id: "container-id", - Names: []string{"/container-name1", "/container-name2"}, + container := &cluster.Container{ + Container: dockerclient.Container{ + Id: "container-id", + Names: []string{"/container-name1", "/container-name2"}, + }, + Config: cluster.BuildContainerConfig(dockerclient.ContainerConfig{ + Labels: map[string]string{ + "com.docker.swarm.id": "swarm-id", + }, + }), } n := createEngine(t, "test-engine", container) @@ -45,4 +53,8 @@ func TestContainerLookup(t *testing.T) { // Container engine/name matching. assert.NotNil(t, c.Container("test-engine/container-name1")) assert.NotNil(t, c.Container("test-engine/container-name2")) + // Swarm ID lookup. + assert.NotNil(t, c.Container("swarm-id")) + // Swarm ID prefix lookup. + assert.NotNil(t, c.Container("swarm-")) } diff --git a/test/integration/swarm_id.bats b/test/integration/swarm_id.bats new file mode 100644 index 0000000000..dda16401da --- /dev/null +++ b/test/integration/swarm_id.bats @@ -0,0 +1,35 @@ +#!/usr/bin/env bats + +load helpers + +function teardown() { + swarm_manage_cleanup + stop_docker +} + +@test "swarm id generation" { + start_docker_with_busybox 1 + swarm_manage + + # Create a dummy container just so we interfere with the tests. + # This one won't be used. + docker_swarm run -d busybox true + + # Create a container and get its Swarm ID back. + id=$(docker_swarm run -d -i busybox sh -c "head -n 1; echo output") + swarm_id=$(docker_swarm inspect -f '{{ index .Config.Labels "com.docker.swarm.id" }}' "$id") + + # Make sure we got a valid Swarm ID. + [[ "${#swarm_id}" -eq 64 ]] + [[ "$id" != "$swarm_id" ]] + + # API operations should work with Swarm IDs as well as Container IDs. + [[ $(docker_swarm inspect -f "{{ .Id }}" "$swarm_id") == "$id" ]] + # These should work with a Swarm ID. + docker_swarm logs "$swarm_id" + docker_swarm commit "$swarm_id" + attach_output=$(echo input | docker_swarm attach "$swarm_id") + + # `docker ps` should be able to filter by Swarm ID using the label. + [[ $(docker_swarm ps -a -q --no-trunc --filter="label=com.docker.swarm.id=$swarm_id") == "$id" ]] +}