From 0b9a3c86a26b4b7eb463a6c73cb030bc851fdd64 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Sun, 31 Mar 2013 02:02:01 -0700 Subject: [PATCH] Show shorthand container IDs for convenience. Shorthand IDs (or any non-conflicting prefix) can be used to lookup containers --- commands.go | 12 ++++---- container.go | 12 ++++++++ runtime.go | 12 +++++++- utils.go | 64 ++++++++++++++++++++++++++++++++++++++++ utils_test.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 175 insertions(+), 7 deletions(-) diff --git a/commands.go b/commands.go index 2dcccb48a4..30c23ed301 100644 --- a/commands.go +++ b/commands.go @@ -226,7 +226,7 @@ func (srv *Server) CmdStop(stdin io.ReadCloser, stdout io.Writer, args ...string if err := container.Stop(); err != nil { return err } - fmt.Fprintln(stdout, container.Id) + fmt.Fprintln(stdout, container.ShortId()) } else { return fmt.Errorf("No such container: %s", name) } @@ -248,7 +248,7 @@ func (srv *Server) CmdRestart(stdin io.ReadCloser, stdout io.Writer, args ...str if err := container.Restart(); err != nil { return err } - fmt.Fprintln(stdout, container.Id) + fmt.Fprintln(stdout, container.ShortId()) } else { return fmt.Errorf("No such container: %s", name) } @@ -270,7 +270,7 @@ func (srv *Server) CmdStart(stdin io.ReadCloser, stdout io.Writer, args ...strin if err := container.Start(); err != nil { return err } - fmt.Fprintln(stdout, container.Id) + fmt.Fprintln(stdout, container.ShortId()) } else { return fmt.Errorf("No such container: %s", name) } @@ -659,7 +659,7 @@ func (srv *Server) CmdPs(stdin io.ReadCloser, stdout io.Writer, args ...string) command = Trunc(command, 20) } for idx, field := range []string{ - /* ID */ container.Id, + /* ID */ container.ShortId(), /* IMAGE */ srv.runtime.repositories.ImageName(container.Image), /* COMMAND */ command, /* CREATED */ HumanDuration(time.Now().Sub(container.Created)) + " ago", @@ -674,7 +674,7 @@ func (srv *Server) CmdPs(stdin io.ReadCloser, stdout io.Writer, args ...string) } w.Write([]byte{'\n'}) } else { - stdout.Write([]byte(container.Id + "\n")) + stdout.Write([]byte(container.ShortId() + "\n")) } } if !*quiet { @@ -965,7 +965,7 @@ func (srv *Server) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) if err := container.Start(); err != nil { return err } - fmt.Fprintln(stdout, container.Id) + fmt.Fprintln(stdout, container.ShortId()) } return nil } diff --git a/container.go b/container.go index ccab0f7494..a03614c011 100644 --- a/container.go +++ b/container.go @@ -555,6 +555,18 @@ func (container *Container) Unmount() error { return Unmount(container.RootfsPath()) } +// ShortId returns a shorthand version of the container's id for convenience. +// A collision with other container shorthands is very unlikely, but possible. +// In case of a collision a lookup with Runtime.Get() will fail, and the caller +// will need to use a langer prefix, or the full-length container Id. +func (container *Container) ShortId() string { + shortLen := 12 + if len(container.Id) < shortLen { + shortLen = len(container.Id) + } + return container.Id[:shortLen] +} + func (container *Container) logPath(name string) string { return path.Join(container.root, fmt.Sprintf("%s-%s.log", container.Id, name)) } diff --git a/runtime.go b/runtime.go index 37726da0ec..9122f0c664 100644 --- a/runtime.go +++ b/runtime.go @@ -21,6 +21,7 @@ type Runtime struct { graph *Graph repositories *TagStore authConfig *auth.AuthConfig + idIndex *TruncIndex } var sysInitPath string @@ -47,7 +48,11 @@ func (runtime *Runtime) getContainerElement(id string) *list.Element { return nil } -func (runtime *Runtime) Get(id string) *Container { +func (runtime *Runtime) Get(name string) *Container { + id, err := runtime.idIndex.Get(name) + if err != nil { + return nil + } e := runtime.getContainerElement(id) if e == nil { return nil @@ -72,6 +77,7 @@ func (runtime *Runtime) Create(config *Config) (*Container, error) { // Generate id id := GenerateId() // Generate default hostname + // FIXME: the lxc template no longer needs to set a default hostname if config.Hostname == "" { config.Hostname = id[:12] } @@ -142,6 +148,7 @@ func (runtime *Runtime) Register(container *Container) error { } // done runtime.containers.PushBack(container) + runtime.idIndex.Add(container.Id) return nil } @@ -171,6 +178,7 @@ func (runtime *Runtime) Destroy(container *Container) error { } } // Deregister the container before removing its directory, to avoid race conditions + runtime.idIndex.Delete(container.Id) runtime.containers.Remove(element) if err := os.RemoveAll(container.root); err != nil { return fmt.Errorf("Unable to remove filesystem for %v: %v", container.Id, err) @@ -222,6 +230,7 @@ func (runtime *Runtime) restore() error { return nil } +// FIXME: harmonize with NewGraph() func NewRuntime() (*Runtime, error) { return NewRuntimeFromDirectory("/var/lib/docker") } @@ -259,6 +268,7 @@ func NewRuntimeFromDirectory(root string) (*Runtime, error) { graph: g, repositories: repositories, authConfig: authConfig, + idIndex: NewTruncIndex(), } if err := runtime.restore(); err != nil { diff --git a/utils.go b/utils.go index 3c6c3c91ee..a87544ff3c 100644 --- a/utils.go +++ b/utils.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "github.com/dotcloud/docker/rcli" + "index/suffixarray" "io" "io/ioutil" "net/http" @@ -270,3 +271,66 @@ func getTotalUsedFds() int { } return -1 } + +// TruncIndex allows the retrieval of string identifiers by any of their unique prefixes. +// This is used to retrieve image and container IDs by more convenient shorthand prefixes. +type TruncIndex struct { + index *suffixarray.Index + ids map[string]bool + bytes []byte +} + +func NewTruncIndex() *TruncIndex { + return &TruncIndex{ + index: suffixarray.New([]byte{' '}), + ids: make(map[string]bool), + bytes: []byte{' '}, + } +} + +func (idx *TruncIndex) Add(id string) error { + if strings.Contains(id, " ") { + return fmt.Errorf("Illegal character: ' '") + } + if _, exists := idx.ids[id]; exists { + return fmt.Errorf("Id already exists: %s", id) + } + idx.ids[id] = true + idx.bytes = append(idx.bytes, []byte(id+" ")...) + idx.index = suffixarray.New(idx.bytes) + return nil +} + +func (idx *TruncIndex) Delete(id string) error { + if _, exists := idx.ids[id]; !exists { + return fmt.Errorf("No such id: %s", id) + } + before, after, err := idx.lookup(id) + if err != nil { + return err + } + delete(idx.ids, id) + idx.bytes = append(idx.bytes[:before], idx.bytes[after:]...) + idx.index = suffixarray.New(idx.bytes) + return nil +} + +func (idx *TruncIndex) lookup(s string) (int, int, error) { + offsets := idx.index.Lookup([]byte(" "+s), -1) + //log.Printf("lookup(%s): %v (index bytes: '%s')\n", s, offsets, idx.index.Bytes()) + if offsets == nil || len(offsets) == 0 || len(offsets) > 1 { + return -1, -1, fmt.Errorf("No such id: %s", s) + } + offsetBefore := offsets[0] + 1 + offsetAfter := offsetBefore + strings.Index(string(idx.bytes[offsetBefore:]), " ") + return offsetBefore, offsetAfter, nil +} + +func (idx *TruncIndex) Get(s string) (string, error) { + before, after, err := idx.lookup(s) + //log.Printf("Get(%s) bytes=|%s| before=|%d| after=|%d|\n", s, idx.bytes, before, after) + if err != nil { + return "", err + } + return string(idx.bytes[before:after]), err +} diff --git a/utils_test.go b/utils_test.go index dbdcda434c..192b042ba2 100644 --- a/utils_test.go +++ b/utils_test.go @@ -124,3 +124,85 @@ func TestWriteBroadcaster(t *testing.T) { writer.Close() } + +// Test the behavior of TruncIndex, an index for querying IDs from a non-conflicting prefix. +func TestTruncIndex(t *testing.T) { + index := NewTruncIndex() + // Get on an empty index + if _, err := index.Get("foobar"); err == nil { + t.Fatal("Get on an empty index should return an error") + } + + // Spaces should be illegal in an id + if err := index.Add("I have a space"); err == nil { + t.Fatalf("Adding an id with ' ' should return an error") + } + + id := "99b36c2c326ccc11e726eee6ee78a0baf166ef96" + // Add an id + if err := index.Add(id); err != nil { + t.Fatal(err) + } + // Get a non-existing id + assertIndexGet(t, index, "abracadabra", "", true) + // Get the exact id + assertIndexGet(t, index, id, id, false) + // The first letter should match + assertIndexGet(t, index, id[:1], id, false) + // The first half should match + assertIndexGet(t, index, id[:len(id)/2], id, false) + // The second half should NOT match + assertIndexGet(t, index, id[len(id)/2:], "", true) + + id2 := id[:6] + "blabla" + // Add an id + if err := index.Add(id2); err != nil { + t.Fatal(err) + } + // Both exact IDs should work + assertIndexGet(t, index, id, id, false) + assertIndexGet(t, index, id2, id2, false) + + // 6 characters or less should conflict + assertIndexGet(t, index, id[:6], "", true) + assertIndexGet(t, index, id[:4], "", true) + assertIndexGet(t, index, id[:1], "", true) + + // 7 characters should NOT conflict + assertIndexGet(t, index, id[:7], id, false) + assertIndexGet(t, index, id2[:7], id2, false) + + // Deleting a non-existing id should return an error + if err := index.Delete("non-existing"); err == nil { + t.Fatalf("Deleting a non-existing id should return an error") + } + + // Deleting id2 should remove conflicts + if err := index.Delete(id2); err != nil { + t.Fatal(err) + } + // id2 should no longer work + assertIndexGet(t, index, id2, "", true) + assertIndexGet(t, index, id2[:7], "", true) + assertIndexGet(t, index, id2[:11], "", true) + + // conflicts between id and id2 should be gone + assertIndexGet(t, index, id[:6], id, false) + assertIndexGet(t, index, id[:4], id, false) + assertIndexGet(t, index, id[:1], id, false) + + // non-conflicting substrings should still not conflict + assertIndexGet(t, index, id[:7], id, false) + assertIndexGet(t, index, id[:15], id, false) + assertIndexGet(t, index, id, id, false) +} + +func assertIndexGet(t *testing.T, index *TruncIndex, input, expectedResult string, expectError bool) { + if result, err := index.Get(input); err != nil && !expectError { + t.Fatalf("Unexpected error getting '%s': %s", input, err) + } else if err == nil && expectError { + t.Fatalf("Getting '%s' should return an error", input) + } else if result != expectedResult { + t.Fatalf("Getting '%s' returned '%s' instead of '%s'", input, result, expectedResult) + } +}