From 784e7647e1fcdf27084bd6a308280e9b769d4249 Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Thu, 26 Sep 2013 14:20:43 -0700 Subject: [PATCH] Add gograph database to handle linking --- api.go | 24 +++ api_params.go | 8 +- commands.go | 36 +++- gograph/gograph.go | 335 ++++++++++++++++++++++++++++++++ gograph/gograph_test.go | 413 ++++++++++++++++++++++++++++++++++++++++ gograph/sort.go | 27 +++ gograph/sort_test.go | 29 +++ gograph/utils.go | 29 +++ links.go | 2 - runtime.go | 13 ++ server.go | 55 +++--- 11 files changed, 936 insertions(+), 35 deletions(-) create mode 100644 gograph/gograph.go create mode 100644 gograph/gograph_test.go create mode 100644 gograph/sort.go create mode 100644 gograph/sort_test.go create mode 100644 gograph/utils.go diff --git a/api.go b/api.go index f7587fc83..0c4d56b2c 100644 --- a/api.go +++ b/api.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "github.com/dotcloud/docker/auth" + "github.com/dotcloud/docker/gograph" "github.com/dotcloud/docker/utils" "github.com/gorilla/mux" "io" @@ -992,6 +993,28 @@ func makeHttpHandler(srv *Server, logging bool, localMethod string, localRoute s } } +func getContainersLinks(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + runtime := srv.runtime + + out := []APILink{} + err := runtime.containerGraph.Walk(func(p string, e *gograph.Entity) error { + container := runtime.Get(e.ID()) + if container != nil { + out = append(out, APILink{ + Path: p, + ContainerID: container.ID, + Image: runtime.repositories.ImageName(container.Image), + }) + } + return nil + }) + if err != nil { + return err + } + + return writeJSON(w, http.StatusOK, out) +} + func createRouter(srv *Server, logging bool) (*mux.Router, error) { r := mux.NewRouter() @@ -1012,6 +1035,7 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) { "/containers/{name:.*}/json": getContainersByName, "/containers/{name:.*}/top": getContainersTop, "/containers/{name:.*}/attach/ws": wsContainersAttach, + "/containers/links": getContainersLinks, }, "POST": { "/auth": postAuth, diff --git a/api_params.go b/api_params.go index cfa339b76..5242d0222 100644 --- a/api_params.go +++ b/api_params.go @@ -42,7 +42,6 @@ type APIRmi struct { } type APIContainers struct { - Name string ID string `json:"Id"` Image string Command string @@ -51,6 +50,7 @@ type APIContainers struct { Ports []APIPort SizeRw int64 SizeRootFs int64 + Names []string } func (self *APIContainers) ToLegacy() APIContainersOld { @@ -121,3 +121,9 @@ type APICopy struct { Resource string HostPath string } + +type APILink struct { + Path string + ContainerID string + Image string +} diff --git a/commands.go b/commands.go index 7b5eb1055..ee17417a9 100644 --- a/commands.go +++ b/commands.go @@ -98,6 +98,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error { {"kill", "Kill a running container"}, {"login", "Register or Login to the docker registry server"}, {"logs", "Fetch the logs of a container"}, + {"ls", "List links for containers"}, {"port", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT"}, {"top", "Lookup the running processes of a container"}, {"ps", "List containers"}, @@ -1073,7 +1074,7 @@ func (cli *DockerCli) CmdPs(args ...string) error { } w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) if !*quiet { - fmt.Fprint(w, "NAME\tID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS") + fmt.Fprint(w, "ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") if *size { fmt.Fprintln(w, "\tSIZE") } else { @@ -1082,11 +1083,12 @@ func (cli *DockerCli) CmdPs(args ...string) error { } for _, out := range outs { + names := strings.Join(out.Names, ",") if !*quiet { if *noTrunc { - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\t%s\t", out.Name, out.ID, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports)) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\t%s\t", out.ID, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports), names) } else { - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\t%s\t", out.Name, utils.TruncateID(out.ID), out.Image, utils.Trunc(out.Command, 20), utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports)) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\t%s\t", utils.TruncateID(out.ID), out.Image, utils.Trunc(out.Command, 20), utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports), names) } if *size { if out.SizeRootFs > 0 { @@ -1112,6 +1114,34 @@ func (cli *DockerCli) CmdPs(args ...string) error { return nil } +func (cli *DockerCli) CmdLs(args ...string) error { + cmd := Subcmd("ls", "", "List links for containers") + if err := cmd.Parse(args); err != nil { + return nil + } + + body, _, err := cli.call("GET", "/containers/links", nil) + if err != nil { + return err + } + var links []APILink + if err := json.Unmarshal(body, &links); err != nil { + return err + } + + w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) + fmt.Fprintf(w, "NAME\tID\tIMAGE") + fmt.Fprintf(w, "\n") + + for _, link := range links { + fmt.Fprintf(w, "%s\t%s\t%s", link.Path, link.ContainerID, link.Image) + fmt.Fprintf(w, "\n") + } + w.Flush() + + return nil +} + func (cli *DockerCli) CmdCommit(args ...string) error { cmd := Subcmd("commit", "[OPTIONS] CONTAINER [REPOSITORY [TAG]]", "Create a new image from a container's changes") flComment := cmd.String("m", "", "Commit message") diff --git a/gograph/gograph.go b/gograph/gograph.go new file mode 100644 index 000000000..17edfadf1 --- /dev/null +++ b/gograph/gograph.go @@ -0,0 +1,335 @@ +package gograph + +import ( + "fmt" + "path" + "sync" +) + +// Entity with a unique id and user defined value +type Entity struct { + id string + Value interface{} +} + +// An Edge connects two entities together +type Edge struct { + EntityID string + Name string + ParentID string +} + +type Entities map[string]*Entity +type Edges []*Edge + +type WalkFunc func(fullPath string, entity *Entity) error + +// Graph database for storing entities and their relationships +type Database struct { + entities Entities + edges Edges + mux sync.Mutex + + rootID string +} + +// Create a new graph database initialized with a root entity +func NewDatabase(rootPath, rootId string) (*Database, error) { + db := &Database{Entities{}, Edges{}, sync.Mutex{}, rootId} + e := &Entity{ + id: rootId, + } + db.entities[rootId] = e + + edge := &Edge{ + EntityID: rootId, + Name: "/", + } + db.edges = append(db.edges, edge) + + return db, nil +} + +// Set the entity id for a given path +func (db *Database) Set(fullPath, id string) (*Entity, error) { + db.mux.Lock() + defer db.mux.Unlock() + + e, exists := db.entities[id] + if !exists { + e = &Entity{ + id: id, + } + db.entities[id] = e + } + + parentPath, name := splitPath(fullPath) + if err := db.setEdge(parentPath, name, e); err != nil { + return nil, err + } + return e, nil +} + +func (db *Database) setEdge(parentPath, name string, e *Entity) error { + parent := db.Get(parentPath) + if parent == nil { + return fmt.Errorf("Parent does not exist for path: %s", parentPath) + } + if parent.id == e.id { + return fmt.Errorf("Cannot set self as child") + } + + edge := &Edge{ + ParentID: parent.id, + EntityID: e.id, + Name: name, + } + if db.edges.Exists(parent.id, name) { + return fmt.Errorf("Relationship already exists for %s/%s", parentPath, name) + } + db.edges = append(db.edges, edge) + return nil +} + +// Return the root "/" entity for the database +func (db *Database) RootEntity() *Entity { + return db.entities[db.rootID] +} + +// Return the entity for a given path +func (db *Database) Get(name string) *Entity { + e := db.RootEntity() + // We always know the root name so return it if + // it is requested + if name == "/" { + return e + } + + parts := split(name) + for i := 1; i < len(parts); i++ { + p := parts[i] + + next := db.child(e, p) + if next == nil { + return nil + } + e = next + + } + return e +} + +// List all entities by from the name +// The key will be the full path of the entity +func (db *Database) List(name string, depth int) Entities { + out := Entities{} + for c := range db.children(name, depth) { + out[c.FullPath] = c.Entity + } + return out +} + +func (db *Database) Walk(walkFunc WalkFunc) error { + parent := db.RootEntity() + if err := walkFunc("/", parent); err != nil { + return err + } + for c := range db.children("/", -1) { + if err := walkFunc(c.FullPath, c.Entity); err != nil { + return err + } + } + return nil +} + +// Return the refrence count for a specified id +func (db *Database) Refs(id string) int { + return len(db.RefPaths(id)) +} + +// Return all the id's path references +func (db *Database) RefPaths(id string) Edges { + refs := db.edges.Search(func(e *Edge) bool { + return e.EntityID == id + }) + return refs +} + +// Delete the reference to an entity at a given path +func (db *Database) Delete(name string) error { + if name == "/" { + return fmt.Errorf("Cannot delete root entity") + } + db.mux.Lock() + defer db.mux.Unlock() + + parentPath, n := splitPath(name) + parent := db.Get(parentPath) + if parent == nil { + return fmt.Errorf("Cannot find parent for %s", parentPath) + } + edge, i := db.edges.Get(parent.id, n) + if edge == nil { + return fmt.Errorf("Edge does not exist at %s", name) + } + db.deleteEdgeAtIndex(i) + + return nil +} + +func (db *Database) deleteEdgeAtIndex(i int) { + db.edges[len(db.edges)-1], db.edges[i], db.edges = nil, db.edges[len(db.edges)-1], db.edges[:len(db.edges)-1] +} + +// Remove the entity with the specified id +// Walk the graph to make sure all references to the entity +// are removed and return the number of references removed +func (db *Database) Purge(id string) (int, error) { + db.mux.Lock() + defer db.mux.Unlock() + + getIndex := func(e *Edge) int { + for i, edge := range db.edges { + if edge.EntityID == e.EntityID && + edge.Name == e.Name && + edge.ParentID == e.ParentID { + return i + } + } + return -1 + } + + refsToDelete := db.RefPaths(id) + for i, e := range refsToDelete { + index := getIndex(e) + if index == -1 { + return i + 1, fmt.Errorf("Cannot find index for %s %s", e.ParentID, e.Name) + } + db.deleteEdgeAtIndex(index) + } + return len(refsToDelete), nil +} + +// Rename an edge for a given path +func (db *Database) Rename(currentName, newName string) error { + parentPath, name := splitPath(currentName) + newParentPath, newEdgeName := splitPath(newName) + + if parentPath != newParentPath { + return fmt.Errorf("Cannot rename when root paths do not match %s != %s", parentPath, newParentPath) + } + + db.mux.Lock() + defer db.mux.Unlock() + + parent := db.Get(parentPath) + if parent == nil { + return fmt.Errorf("Cannot locate parent for %s", currentName) + } + edge, _ := db.edges.Get(parent.id, name) + if edge == nil { + return fmt.Errorf("Cannot locate edge for %s %s", parent.id, name) + } + edge.Name = newEdgeName + + return nil +} + +type WalkMeta struct { + Parent *Entity + Entity *Entity + FullPath string + Edge *Edge +} + +func (db *Database) children(name string, depth int) <-chan WalkMeta { + out := make(chan WalkMeta) + e := db.Get(name) + + if e == nil { + close(out) + return out + } + + go func() { + for _, edge := range db.edges { + if edge.ParentID == e.id { + child := db.entities[edge.EntityID] + + meta := WalkMeta{ + Parent: e, + Entity: child, + FullPath: path.Join(name, edge.Name), + Edge: edge, + } + out <- meta + if depth == 0 { + continue + } + nDepth := depth + if depth != -1 { + nDepth -= 1 + } + sc := db.children(meta.FullPath, nDepth) + for c := range sc { + out <- c + } + } + } + close(out) + }() + return out +} + +// Return the entity based on the parent path and name +func (db *Database) child(parent *Entity, name string) *Entity { + edge, _ := db.edges.Get(parent.id, name) + if edge == nil { + return nil + } + return db.entities[edge.EntityID] +} + +// Return the id used to reference this entity +func (e *Entity) ID() string { + return e.id +} + +// Return the paths sorted by depth +func (e Entities) Paths() []string { + out := make([]string, len(e)) + var i int + for k := range e { + out[i] = k + i++ + } + sortByDepth(out) + + return out +} + +// Checks if an edge with the specified parent id and name exist in the slice +func (e Edges) Exists(parendId, name string) bool { + edge, _ := e.Get(parendId, name) + return edge != nil +} + +// Returns the edge and index in the slice with the specified parent id and name +func (e Edges) Get(parentId, name string) (*Edge, int) { + for i, edge := range e { + if edge.ParentID == parentId && edge.Name == name { + return edge, i + } + } + return nil, -1 +} + +func (e Edges) Search(predicate func(edge *Edge) bool) Edges { + out := Edges{} + for _, edge := range e { + if predicate(edge) { + out = append(out, edge) + } + } + return out +} diff --git a/gograph/gograph_test.go b/gograph/gograph_test.go new file mode 100644 index 000000000..8b2234bbf --- /dev/null +++ b/gograph/gograph_test.go @@ -0,0 +1,413 @@ +package gograph + +import ( + "os" + "strconv" + "testing" +) + +func newTestDb(t *testing.T) *Database { + db, err := NewDatabase(os.TempDir(), "0") + if err != nil { + t.Fatal(err) + } + return db +} + +func TestNewDatabase(t *testing.T) { + db := newTestDb(t) + if db == nil { + t.Fatal("Datbase should not be nil") + } +} + +func TestCreateRootEnity(t *testing.T) { + db := newTestDb(t) + root := db.RootEntity() + if root == nil { + t.Fatal("Root entity should not be nil") + } +} + +func TestGetRootEntity(t *testing.T) { + db := newTestDb(t) + + e := db.Get("/") + if e == nil { + t.Fatal("Entity should not be nil") + } + if e.ID() != "0" { + t.Fatalf("Enity id should be 0, got %s", e.ID()) + } +} + +func TestSetEntityWithDifferentName(t *testing.T) { + db := newTestDb(t) + + db.Set("/test", "1") + if _, err := db.Set("/other", "1"); err != nil { + t.Fatal(err) + } +} + +func TestCreateChild(t *testing.T) { + db := newTestDb(t) + + child, err := db.Set("/db", "1") + if err != nil { + t.Fatal(err) + } + if child == nil { + t.Fatal("Child should not be nil") + } + if child.ID() != "1" { + t.Fail() + } +} + +func TestListAllRootChildren(t *testing.T) { + db := newTestDb(t) + + for i := 1; i < 6; i++ { + a := strconv.Itoa(i) + if _, err := db.Set("/"+a, a); err != nil { + t.Fatal(err) + } + } + entries := db.List("/", -1) + if len(entries) != 5 { + t.Fatalf("Expect 5 entries for / got %d", len(entries)) + } +} + +func TestListAllSubChildren(t *testing.T) { + db := newTestDb(t) + + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/db/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + entries := db.List("/webapp", 1) + if len(entries) != 3 { + t.Fatalf("Expect 3 entries for / got %d", len(entries)) + } + + entries = db.List("/webapp", 0) + if len(entries) != 2 { + t.Fatalf("Expect 2 entries for / got %d", len(entries)) + } +} + +func TestAddSelfAsChild(t *testing.T) { + db := newTestDb(t) + + child, err := db.Set("/test", "1") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/test/other", child.ID()); err == nil { + t.Fatal("Error should not be nil") + } +} + +func TestAddChildToNonExistantRoot(t *testing.T) { + db := newTestDb(t) + + if _, err := db.Set("/myapp", "1"); err != nil { + t.Fatal(err) + } + + if _, err := db.Set("/myapp/proxy/db", "2"); err == nil { + t.Fatal("Error should not be nil") + } +} + +func TestWalkAll(t *testing.T) { + db := newTestDb(t) + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/db/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + child5, err := db.Set("/gograph", "5") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil { + t.Fatal(err) + } + + if err := db.Walk(func(p string, e *Entity) error { + t.Logf("Path: %s Entity: %s", p, e.ID()) + return nil + }); err != nil { + t.Fatal(err) + } +} + +func TestGetEntityByPath(t *testing.T) { + db := newTestDb(t) + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/db/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + child5, err := db.Set("/gograph", "5") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil { + t.Fatal(err) + } + + entity := db.Get("/webapp/db/logs") + if entity == nil { + t.Fatal("Entity should not be nil") + } + if entity.ID() != "4" { + t.Fatalf("Expected to get entity with id 4, got %s", entity.ID()) + } +} + +func TestEnitiesPaths(t *testing.T) { + db := newTestDb(t) + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/db/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + child5, err := db.Set("/gograph", "5") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil { + t.Fatal(err) + } + + out := db.List("/", -1) + for _, p := range out.Paths() { + t.Log(p) + } +} + +func TestDeleteRootEntity(t *testing.T) { + db := newTestDb(t) + + if err := db.Delete("/"); err == nil { + t.Fatal("Error should not be nil") + } +} + +func TestDeleteEntity(t *testing.T) { + db := newTestDb(t) + _, err := db.Set("/webapp", "1") + if err != nil { + t.Fatal(err) + } + child2, err := db.Set("/db", "2") + if err != nil { + t.Fatal(err) + } + child4, err := db.Set("/logs", "4") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/db/logs", child4.ID()); err != nil { + t.Fatal(err) + } + + child3, err := db.Set("/sentry", "3") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/sentry", child3.ID()); err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/db", child2.ID()); err != nil { + t.Fatal(err) + } + + child5, err := db.Set("/gograph", "5") + if err != nil { + t.Fatal(err) + } + if _, err := db.Set("/webapp/same-ref-diff-name", child5.ID()); err != nil { + t.Fatal(err) + } + + if err := db.Delete("/webapp/sentry"); err != nil { + t.Fatal(err) + } + entity := db.Get("/webapp/sentry") + if entity != nil { + t.Fatal("Entity /webapp/sentry should be nil") + } +} + +func TestCountRefs(t *testing.T) { + db := newTestDb(t) + + db.Set("/webapp", "1") + + if db.Refs("1") != 1 { + t.Fatal("Expect reference count to be 1") + } + + db.Set("/db", "2") + db.Set("/webapp/db", "2") + if db.Refs("2") != 2 { + t.Fatal("Expect reference count to be 2") + } +} + +func TestPurgeId(t *testing.T) { + db := newTestDb(t) + + db.Set("/webapp", "1") + + if db.Refs("1") != 1 { + t.Fatal("Expect reference count to be 1") + } + + db.Set("/db", "2") + db.Set("/webapp/db", "2") + + count, err := db.Purge("2") + if err != nil { + t.Fatal(err) + } + if count != 2 { + t.Fatal("Expected 2 references to be removed") + } +} + +func TestRename(t *testing.T) { + db := newTestDb(t) + + db.Set("/webapp", "1") + + if db.Refs("1") != 1 { + t.Fatal("Expect reference count to be 1") + } + + db.Set("/db", "2") + db.Set("/webapp/db", "2") + + if db.Get("/webapp/db") == nil { + t.Fatal("Cannot find entity at path /webapp/db") + } + + if err := db.Rename("/webapp/db", "/webapp/newdb"); err != nil { + t.Fatal(err) + } + if db.Get("/webapp/db") != nil { + t.Fatal("Entity should not exist at /webapp/db") + } + if db.Get("/webapp/newdb") == nil { + t.Fatal("Cannot find entity at path /webapp/newdb") + } + +} + +func TestCreateMultipleNames(t *testing.T) { + db := newTestDb(t) + + db.Set("/db", "1") + if _, err := db.Set("/myapp", "1"); err != nil { + t.Fatal(err) + } + + db.Walk(func(p string, e *Entity) error { + t.Logf("%s\n", p) + return nil + }) +} diff --git a/gograph/sort.go b/gograph/sort.go new file mode 100644 index 000000000..cc936cb84 --- /dev/null +++ b/gograph/sort.go @@ -0,0 +1,27 @@ +package gograph + +import "sort" + +type pathSorter struct { + paths []string + by func(i, j string) bool +} + +func sortByDepth(paths []string) { + s := &pathSorter{paths, func(i, j string) bool { + return pathDepth(i) > pathDepth(j) + }} + sort.Sort(s) +} + +func (s *pathSorter) Len() int { + return len(s.paths) +} + +func (s *pathSorter) Swap(i, j int) { + s.paths[i], s.paths[j] = s.paths[j], s.paths[i] +} + +func (s *pathSorter) Less(i, j int) bool { + return s.by(s.paths[i], s.paths[j]) +} diff --git a/gograph/sort_test.go b/gograph/sort_test.go new file mode 100644 index 000000000..40431039a --- /dev/null +++ b/gograph/sort_test.go @@ -0,0 +1,29 @@ +package gograph + +import ( + "testing" +) + +func TestSort(t *testing.T) { + paths := []string{ + "/", + "/myreallylongname", + "/app/db", + } + + sortByDepth(paths) + + if len(paths) != 3 { + t.Fatalf("Expected 3 parts got %d", len(paths)) + } + + if paths[0] != "/app/db" { + t.Fatalf("Expected /app/db got %s", paths[0]) + } + if paths[1] != "/myreallylongname" { + t.Fatalf("Expected /myreallylongname got %s", paths[1]) + } + if paths[2] != "/" { + t.Fatalf("Expected / got %s", paths[2]) + } +} diff --git a/gograph/utils.go b/gograph/utils.go new file mode 100644 index 000000000..72044e49d --- /dev/null +++ b/gograph/utils.go @@ -0,0 +1,29 @@ +package gograph + +import ( + "path" + "strings" +) + +// Split p on / +func split(p string) []string { + return strings.Split(p, "/") +} + +// Returns the depth or number of / in a given path +func pathDepth(p string) int { + parts := split(p) + if len(parts) == 2 && parts[1] == "" { + return 1 + } + return len(parts) +} + +func splitPath(p string) (parent, name string) { + parent, name = path.Split(p) + l := len(parent) + if parent[l-1] == '/' { + parent = parent[:l-1] + } + return +} diff --git a/links.go b/links.go index cd70f7231..8c3d08efd 100644 --- a/links.go +++ b/links.go @@ -7,8 +7,6 @@ import ( "strings" ) -// A Link represents a connection between two containers -// for a specific port on a specific bridge interface type Link struct { FromID string ToID string diff --git a/runtime.go b/runtime.go index 66fda5838..f9c87a69b 100644 --- a/runtime.go +++ b/runtime.go @@ -3,6 +3,7 @@ package docker import ( "container/list" "fmt" + "github.com/dotcloud/docker/gograph" "github.com/dotcloud/docker/utils" "io" "io/ioutil" @@ -36,6 +37,7 @@ type Runtime struct { srv *Server config *DaemonConfig links *LinkRepository + containerGraph *gograph.Database } var sysInitPath string @@ -321,6 +323,12 @@ func (runtime *Runtime) Create(config *Config) (*Container, []string, error) { // Generate id id := GenerateID() + + // Set the default enitity in the graph + if _, err := runtime.containerGraph.Set(fmt.Sprintf("/%s", id), id); err != nil { + return nil, nil, err + } + // Generate default hostname // FIXME: the lxc template no longer needs to set a default hostname if config.Hostname == "" { @@ -503,6 +511,10 @@ func NewRuntimeFromDirectory(config *DaemonConfig) (*Runtime, error) { if err != nil { return nil, err } + graph, err := gograph.NewDatabase("", "engine") + if err != nil { + return nil, err + } runtime := &Runtime{ repository: runtimeRepo, @@ -515,6 +527,7 @@ func NewRuntimeFromDirectory(config *DaemonConfig) (*Runtime, error) { volumes: volumes, config: config, links: links, + containerGraph: graph, } if err := runtime.restore(); err != nil { diff --git a/server.go b/server.go index c90ded544..39d01dd21 100644 --- a/server.go +++ b/server.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "github.com/dotcloud/docker/auth" + "github.com/dotcloud/docker/gograph" "github.com/dotcloud/docker/registry" "github.com/dotcloud/docker/utils" "io" @@ -357,7 +358,7 @@ func (srv *Server) ContainerChanges(name string) ([]Change, error) { func (srv *Server) Containers(all, size bool, n int, since, before string) []APIContainers { var foundBefore bool var displayed int - retContainers := make(map[string]APIContainers) + out := []APIContainers{} for _, container := range srv.runtime.List() { if !container.State.Running && !all && n == -1 && since == "" && before == "" { @@ -379,39 +380,35 @@ func (srv *Server) Containers(all, size bool, n int, since, before string) []API break } displayed++ - - c := APIContainers{ - ID: container.ID, - } - c.Name = utils.TruncateID(container.ID) - - c.Image = srv.runtime.repositories.ImageName(container.Image) - c.Command = fmt.Sprintf("%s %s", container.Path, strings.Join(container.Args, " ")) - c.Created = container.Created.Unix() - c.Status = container.State.String() - c.Ports = container.NetworkSettings.PortMappingAPI() - if size { - c.SizeRw, c.SizeRootFs = container.GetSize() - } - retContainers[utils.TruncateID(c.ID)] = c - } - out := make([]APIContainers, len(retContainers)) - var i int - for _, v := range retContainers { - out[i] = v - i++ - } - - // Add links to result - for _, link := range srv.runtime.links.GetAll() { - cp := retContainers[link.ToID] - c := cp - c.Name = fmt.Sprintf("%s/%s", link.FromID, link.Alias) + c := createAPIContainer(container, size, srv.runtime) out = append(out, c) } return out } +func createAPIContainer(container *Container, size bool, runtime *Runtime) APIContainers { + c := APIContainers{ + ID: container.ID, + } + names := []string{} + runtime.containerGraph.Walk(func(p string, e *gograph.Entity) error { + if e.ID() == container.ID { + names = append(names, p) + } + return nil + }) + c.Names = names + + c.Image = runtime.repositories.ImageName(container.Image) + c.Command = fmt.Sprintf("%s %s", container.Path, strings.Join(container.Args, " ")) + c.Created = container.Created.Unix() + c.Status = container.State.String() + c.Ports = container.NetworkSettings.PortMappingAPI() + if size { + c.SizeRw, c.SizeRootFs = container.GetSize() + } + return c +} func (srv *Server) ContainerCommit(name, repo, tag, author, comment string, config *Config) (string, error) { container := srv.runtime.Get(name) if container == nil {