| ... | ... |
@@ -226,7 +226,7 @@ func (srv *Server) CmdStop(stdin io.ReadCloser, stdout io.Writer, args ...string |
| 226 | 226 |
if err := container.Stop(); err != nil {
|
| 227 | 227 |
return err |
| 228 | 228 |
} |
| 229 |
- fmt.Fprintln(stdout, container.Id) |
|
| 229 |
+ fmt.Fprintln(stdout, container.ShortId()) |
|
| 230 | 230 |
} else {
|
| 231 | 231 |
return fmt.Errorf("No such container: %s", name)
|
| 232 | 232 |
} |
| ... | ... |
@@ -248,7 +248,7 @@ func (srv *Server) CmdRestart(stdin io.ReadCloser, stdout io.Writer, args ...str |
| 248 | 248 |
if err := container.Restart(); err != nil {
|
| 249 | 249 |
return err |
| 250 | 250 |
} |
| 251 |
- fmt.Fprintln(stdout, container.Id) |
|
| 251 |
+ fmt.Fprintln(stdout, container.ShortId()) |
|
| 252 | 252 |
} else {
|
| 253 | 253 |
return fmt.Errorf("No such container: %s", name)
|
| 254 | 254 |
} |
| ... | ... |
@@ -270,7 +270,7 @@ func (srv *Server) CmdStart(stdin io.ReadCloser, stdout io.Writer, args ...strin |
| 270 | 270 |
if err := container.Start(); err != nil {
|
| 271 | 271 |
return err |
| 272 | 272 |
} |
| 273 |
- fmt.Fprintln(stdout, container.Id) |
|
| 273 |
+ fmt.Fprintln(stdout, container.ShortId()) |
|
| 274 | 274 |
} else {
|
| 275 | 275 |
return fmt.Errorf("No such container: %s", name)
|
| 276 | 276 |
} |
| ... | ... |
@@ -659,7 +659,7 @@ func (srv *Server) CmdPs(stdin io.ReadCloser, stdout io.Writer, args ...string) |
| 659 | 659 |
command = Trunc(command, 20) |
| 660 | 660 |
} |
| 661 | 661 |
for idx, field := range []string{
|
| 662 |
- /* ID */ container.Id, |
|
| 662 |
+ /* ID */ container.ShortId(), |
|
| 663 | 663 |
/* IMAGE */ srv.runtime.repositories.ImageName(container.Image), |
| 664 | 664 |
/* COMMAND */ command, |
| 665 | 665 |
/* CREATED */ HumanDuration(time.Now().Sub(container.Created)) + " ago", |
| ... | ... |
@@ -674,7 +674,7 @@ func (srv *Server) CmdPs(stdin io.ReadCloser, stdout io.Writer, args ...string) |
| 674 | 674 |
} |
| 675 | 675 |
w.Write([]byte{'\n'})
|
| 676 | 676 |
} else {
|
| 677 |
- stdout.Write([]byte(container.Id + "\n")) |
|
| 677 |
+ stdout.Write([]byte(container.ShortId() + "\n")) |
|
| 678 | 678 |
} |
| 679 | 679 |
} |
| 680 | 680 |
if !*quiet {
|
| ... | ... |
@@ -965,7 +965,7 @@ func (srv *Server) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) |
| 965 | 965 |
if err := container.Start(); err != nil {
|
| 966 | 966 |
return err |
| 967 | 967 |
} |
| 968 |
- fmt.Fprintln(stdout, container.Id) |
|
| 968 |
+ fmt.Fprintln(stdout, container.ShortId()) |
|
| 969 | 969 |
} |
| 970 | 970 |
return nil |
| 971 | 971 |
} |
| ... | ... |
@@ -555,6 +555,18 @@ func (container *Container) Unmount() error {
|
| 555 | 555 |
return Unmount(container.RootfsPath()) |
| 556 | 556 |
} |
| 557 | 557 |
|
| 558 |
+// ShortId returns a shorthand version of the container's id for convenience. |
|
| 559 |
+// A collision with other container shorthands is very unlikely, but possible. |
|
| 560 |
+// In case of a collision a lookup with Runtime.Get() will fail, and the caller |
|
| 561 |
+// will need to use a langer prefix, or the full-length container Id. |
|
| 562 |
+func (container *Container) ShortId() string {
|
|
| 563 |
+ shortLen := 12 |
|
| 564 |
+ if len(container.Id) < shortLen {
|
|
| 565 |
+ shortLen = len(container.Id) |
|
| 566 |
+ } |
|
| 567 |
+ return container.Id[:shortLen] |
|
| 568 |
+} |
|
| 569 |
+ |
|
| 558 | 570 |
func (container *Container) logPath(name string) string {
|
| 559 | 571 |
return path.Join(container.root, fmt.Sprintf("%s-%s.log", container.Id, name))
|
| 560 | 572 |
} |
| ... | ... |
@@ -21,6 +21,7 @@ type Runtime struct {
|
| 21 | 21 |
graph *Graph |
| 22 | 22 |
repositories *TagStore |
| 23 | 23 |
authConfig *auth.AuthConfig |
| 24 |
+ idIndex *TruncIndex |
|
| 24 | 25 |
} |
| 25 | 26 |
|
| 26 | 27 |
var sysInitPath string |
| ... | ... |
@@ -47,7 +48,11 @@ func (runtime *Runtime) getContainerElement(id string) *list.Element {
|
| 47 | 47 |
return nil |
| 48 | 48 |
} |
| 49 | 49 |
|
| 50 |
-func (runtime *Runtime) Get(id string) *Container {
|
|
| 50 |
+func (runtime *Runtime) Get(name string) *Container {
|
|
| 51 |
+ id, err := runtime.idIndex.Get(name) |
|
| 52 |
+ if err != nil {
|
|
| 53 |
+ return nil |
|
| 54 |
+ } |
|
| 51 | 55 |
e := runtime.getContainerElement(id) |
| 52 | 56 |
if e == nil {
|
| 53 | 57 |
return nil |
| ... | ... |
@@ -72,6 +77,7 @@ func (runtime *Runtime) Create(config *Config) (*Container, error) {
|
| 72 | 72 |
// Generate id |
| 73 | 73 |
id := GenerateId() |
| 74 | 74 |
// Generate default hostname |
| 75 |
+ // FIXME: the lxc template no longer needs to set a default hostname |
|
| 75 | 76 |
if config.Hostname == "" {
|
| 76 | 77 |
config.Hostname = id[:12] |
| 77 | 78 |
} |
| ... | ... |
@@ -142,6 +148,7 @@ func (runtime *Runtime) Register(container *Container) error {
|
| 142 | 142 |
} |
| 143 | 143 |
// done |
| 144 | 144 |
runtime.containers.PushBack(container) |
| 145 |
+ runtime.idIndex.Add(container.Id) |
|
| 145 | 146 |
return nil |
| 146 | 147 |
} |
| 147 | 148 |
|
| ... | ... |
@@ -171,6 +178,7 @@ func (runtime *Runtime) Destroy(container *Container) error {
|
| 171 | 171 |
} |
| 172 | 172 |
} |
| 173 | 173 |
// Deregister the container before removing its directory, to avoid race conditions |
| 174 |
+ runtime.idIndex.Delete(container.Id) |
|
| 174 | 175 |
runtime.containers.Remove(element) |
| 175 | 176 |
if err := os.RemoveAll(container.root); err != nil {
|
| 176 | 177 |
return fmt.Errorf("Unable to remove filesystem for %v: %v", container.Id, err)
|
| ... | ... |
@@ -222,6 +230,7 @@ func (runtime *Runtime) restore() error {
|
| 222 | 222 |
return nil |
| 223 | 223 |
} |
| 224 | 224 |
|
| 225 |
+// FIXME: harmonize with NewGraph() |
|
| 225 | 226 |
func NewRuntime() (*Runtime, error) {
|
| 226 | 227 |
return NewRuntimeFromDirectory("/var/lib/docker")
|
| 227 | 228 |
} |
| ... | ... |
@@ -259,6 +268,7 @@ func NewRuntimeFromDirectory(root string) (*Runtime, error) {
|
| 259 | 259 |
graph: g, |
| 260 | 260 |
repositories: repositories, |
| 261 | 261 |
authConfig: authConfig, |
| 262 |
+ idIndex: NewTruncIndex(), |
|
| 262 | 263 |
} |
| 263 | 264 |
|
| 264 | 265 |
if err := runtime.restore(); err != nil {
|
| ... | ... |
@@ -6,6 +6,7 @@ import ( |
| 6 | 6 |
"errors" |
| 7 | 7 |
"fmt" |
| 8 | 8 |
"github.com/dotcloud/docker/rcli" |
| 9 |
+ "index/suffixarray" |
|
| 9 | 10 |
"io" |
| 10 | 11 |
"io/ioutil" |
| 11 | 12 |
"net/http" |
| ... | ... |
@@ -270,3 +271,66 @@ func getTotalUsedFds() int {
|
| 270 | 270 |
} |
| 271 | 271 |
return -1 |
| 272 | 272 |
} |
| 273 |
+ |
|
| 274 |
+// TruncIndex allows the retrieval of string identifiers by any of their unique prefixes. |
|
| 275 |
+// This is used to retrieve image and container IDs by more convenient shorthand prefixes. |
|
| 276 |
+type TruncIndex struct {
|
|
| 277 |
+ index *suffixarray.Index |
|
| 278 |
+ ids map[string]bool |
|
| 279 |
+ bytes []byte |
|
| 280 |
+} |
|
| 281 |
+ |
|
| 282 |
+func NewTruncIndex() *TruncIndex {
|
|
| 283 |
+ return &TruncIndex{
|
|
| 284 |
+ index: suffixarray.New([]byte{' '}),
|
|
| 285 |
+ ids: make(map[string]bool), |
|
| 286 |
+ bytes: []byte{' '},
|
|
| 287 |
+ } |
|
| 288 |
+} |
|
| 289 |
+ |
|
| 290 |
+func (idx *TruncIndex) Add(id string) error {
|
|
| 291 |
+ if strings.Contains(id, " ") {
|
|
| 292 |
+ return fmt.Errorf("Illegal character: ' '")
|
|
| 293 |
+ } |
|
| 294 |
+ if _, exists := idx.ids[id]; exists {
|
|
| 295 |
+ return fmt.Errorf("Id already exists: %s", id)
|
|
| 296 |
+ } |
|
| 297 |
+ idx.ids[id] = true |
|
| 298 |
+ idx.bytes = append(idx.bytes, []byte(id+" ")...) |
|
| 299 |
+ idx.index = suffixarray.New(idx.bytes) |
|
| 300 |
+ return nil |
|
| 301 |
+} |
|
| 302 |
+ |
|
| 303 |
+func (idx *TruncIndex) Delete(id string) error {
|
|
| 304 |
+ if _, exists := idx.ids[id]; !exists {
|
|
| 305 |
+ return fmt.Errorf("No such id: %s", id)
|
|
| 306 |
+ } |
|
| 307 |
+ before, after, err := idx.lookup(id) |
|
| 308 |
+ if err != nil {
|
|
| 309 |
+ return err |
|
| 310 |
+ } |
|
| 311 |
+ delete(idx.ids, id) |
|
| 312 |
+ idx.bytes = append(idx.bytes[:before], idx.bytes[after:]...) |
|
| 313 |
+ idx.index = suffixarray.New(idx.bytes) |
|
| 314 |
+ return nil |
|
| 315 |
+} |
|
| 316 |
+ |
|
| 317 |
+func (idx *TruncIndex) lookup(s string) (int, int, error) {
|
|
| 318 |
+ offsets := idx.index.Lookup([]byte(" "+s), -1)
|
|
| 319 |
+ //log.Printf("lookup(%s): %v (index bytes: '%s')\n", s, offsets, idx.index.Bytes())
|
|
| 320 |
+ if offsets == nil || len(offsets) == 0 || len(offsets) > 1 {
|
|
| 321 |
+ return -1, -1, fmt.Errorf("No such id: %s", s)
|
|
| 322 |
+ } |
|
| 323 |
+ offsetBefore := offsets[0] + 1 |
|
| 324 |
+ offsetAfter := offsetBefore + strings.Index(string(idx.bytes[offsetBefore:]), " ") |
|
| 325 |
+ return offsetBefore, offsetAfter, nil |
|
| 326 |
+} |
|
| 327 |
+ |
|
| 328 |
+func (idx *TruncIndex) Get(s string) (string, error) {
|
|
| 329 |
+ before, after, err := idx.lookup(s) |
|
| 330 |
+ //log.Printf("Get(%s) bytes=|%s| before=|%d| after=|%d|\n", s, idx.bytes, before, after)
|
|
| 331 |
+ if err != nil {
|
|
| 332 |
+ return "", err |
|
| 333 |
+ } |
|
| 334 |
+ return string(idx.bytes[before:after]), err |
|
| 335 |
+} |
| ... | ... |
@@ -124,3 +124,85 @@ func TestWriteBroadcaster(t *testing.T) {
|
| 124 | 124 |
|
| 125 | 125 |
writer.Close() |
| 126 | 126 |
} |
| 127 |
+ |
|
| 128 |
+// Test the behavior of TruncIndex, an index for querying IDs from a non-conflicting prefix. |
|
| 129 |
+func TestTruncIndex(t *testing.T) {
|
|
| 130 |
+ index := NewTruncIndex() |
|
| 131 |
+ // Get on an empty index |
|
| 132 |
+ if _, err := index.Get("foobar"); err == nil {
|
|
| 133 |
+ t.Fatal("Get on an empty index should return an error")
|
|
| 134 |
+ } |
|
| 135 |
+ |
|
| 136 |
+ // Spaces should be illegal in an id |
|
| 137 |
+ if err := index.Add("I have a space"); err == nil {
|
|
| 138 |
+ t.Fatalf("Adding an id with ' ' should return an error")
|
|
| 139 |
+ } |
|
| 140 |
+ |
|
| 141 |
+ id := "99b36c2c326ccc11e726eee6ee78a0baf166ef96" |
|
| 142 |
+ // Add an id |
|
| 143 |
+ if err := index.Add(id); err != nil {
|
|
| 144 |
+ t.Fatal(err) |
|
| 145 |
+ } |
|
| 146 |
+ // Get a non-existing id |
|
| 147 |
+ assertIndexGet(t, index, "abracadabra", "", true) |
|
| 148 |
+ // Get the exact id |
|
| 149 |
+ assertIndexGet(t, index, id, id, false) |
|
| 150 |
+ // The first letter should match |
|
| 151 |
+ assertIndexGet(t, index, id[:1], id, false) |
|
| 152 |
+ // The first half should match |
|
| 153 |
+ assertIndexGet(t, index, id[:len(id)/2], id, false) |
|
| 154 |
+ // The second half should NOT match |
|
| 155 |
+ assertIndexGet(t, index, id[len(id)/2:], "", true) |
|
| 156 |
+ |
|
| 157 |
+ id2 := id[:6] + "blabla" |
|
| 158 |
+ // Add an id |
|
| 159 |
+ if err := index.Add(id2); err != nil {
|
|
| 160 |
+ t.Fatal(err) |
|
| 161 |
+ } |
|
| 162 |
+ // Both exact IDs should work |
|
| 163 |
+ assertIndexGet(t, index, id, id, false) |
|
| 164 |
+ assertIndexGet(t, index, id2, id2, false) |
|
| 165 |
+ |
|
| 166 |
+ // 6 characters or less should conflict |
|
| 167 |
+ assertIndexGet(t, index, id[:6], "", true) |
|
| 168 |
+ assertIndexGet(t, index, id[:4], "", true) |
|
| 169 |
+ assertIndexGet(t, index, id[:1], "", true) |
|
| 170 |
+ |
|
| 171 |
+ // 7 characters should NOT conflict |
|
| 172 |
+ assertIndexGet(t, index, id[:7], id, false) |
|
| 173 |
+ assertIndexGet(t, index, id2[:7], id2, false) |
|
| 174 |
+ |
|
| 175 |
+ // Deleting a non-existing id should return an error |
|
| 176 |
+ if err := index.Delete("non-existing"); err == nil {
|
|
| 177 |
+ t.Fatalf("Deleting a non-existing id should return an error")
|
|
| 178 |
+ } |
|
| 179 |
+ |
|
| 180 |
+ // Deleting id2 should remove conflicts |
|
| 181 |
+ if err := index.Delete(id2); err != nil {
|
|
| 182 |
+ t.Fatal(err) |
|
| 183 |
+ } |
|
| 184 |
+ // id2 should no longer work |
|
| 185 |
+ assertIndexGet(t, index, id2, "", true) |
|
| 186 |
+ assertIndexGet(t, index, id2[:7], "", true) |
|
| 187 |
+ assertIndexGet(t, index, id2[:11], "", true) |
|
| 188 |
+ |
|
| 189 |
+ // conflicts between id and id2 should be gone |
|
| 190 |
+ assertIndexGet(t, index, id[:6], id, false) |
|
| 191 |
+ assertIndexGet(t, index, id[:4], id, false) |
|
| 192 |
+ assertIndexGet(t, index, id[:1], id, false) |
|
| 193 |
+ |
|
| 194 |
+ // non-conflicting substrings should still not conflict |
|
| 195 |
+ assertIndexGet(t, index, id[:7], id, false) |
|
| 196 |
+ assertIndexGet(t, index, id[:15], id, false) |
|
| 197 |
+ assertIndexGet(t, index, id, id, false) |
|
| 198 |
+} |
|
| 199 |
+ |
|
| 200 |
+func assertIndexGet(t *testing.T, index *TruncIndex, input, expectedResult string, expectError bool) {
|
|
| 201 |
+ if result, err := index.Get(input); err != nil && !expectError {
|
|
| 202 |
+ t.Fatalf("Unexpected error getting '%s': %s", input, err)
|
|
| 203 |
+ } else if err == nil && expectError {
|
|
| 204 |
+ t.Fatalf("Getting '%s' should return an error", input)
|
|
| 205 |
+ } else if result != expectedResult {
|
|
| 206 |
+ t.Fatalf("Getting '%s' returned '%s' instead of '%s'", input, result, expectedResult)
|
|
| 207 |
+ } |
|
| 208 |
+} |