diff --git a/app/api_topology.go b/app/api_topology.go index 204ddaa65..198d1b162 100644 --- a/app/api_topology.go +++ b/app/api_topology.go @@ -68,7 +68,7 @@ func handleNode(rep xfer.Reporter, t topologyView, w http.ResponseWriter, r *htt http.NotFound(w, r) return } - respondWith(w, http.StatusOK, APINode{Node: render.MakeDetailedNode(rpt, node)}) + respondWith(w, http.StatusOK, APINode{Node: render.MakeDetailedNode(rpt, node, t.isMultiHost)}) } // Individual edges. diff --git a/app/router.go b/app/router.go index 570f223f6..7eabfc8f7 100644 --- a/app/router.go +++ b/app/router.go @@ -107,9 +107,10 @@ var topologyRegistry = map[string]topologyView{ renderer: render.FilterUnconnected{Renderer: render.ProcessWithContainerNameRenderer{}}, }, "applications-by-name": { - human: "by name", - parent: "applications", - renderer: render.FilterUnconnected{Renderer: render.ProcessNameRenderer}, + human: "by name", + parent: "applications", + renderer: render.FilterUnconnected{Renderer: render.ProcessNameRenderer}, + isMultiHost: true, }, "containers": { human: "Containers", @@ -117,9 +118,10 @@ var topologyRegistry = map[string]topologyView{ renderer: render.ContainerRenderer, }, "containers-by-image": { - human: "by image", - parent: "containers", - renderer: render.ContainerImageRenderer, + human: "by image", + parent: "containers", + renderer: render.ContainerImageRenderer, + isMultiHost: true, }, "hosts": { human: "Hosts", @@ -129,7 +131,8 @@ var topologyRegistry = map[string]topologyView{ } type topologyView struct { - human string - parent string - renderer render.Renderer + human string + parent string + renderer render.Renderer + isMultiHost bool // does the view involve information from multiple hosts? } diff --git a/render/detailed_node.go b/render/detailed_node.go index 2d7e9f34b..abb097c24 100644 --- a/render/detailed_node.go +++ b/render/detailed_node.go @@ -72,7 +72,7 @@ func (t tables) Less(i, j int) bool { return t[i].Rank > t[j].Rank } // MakeDetailedNode transforms a renderable node to a detailed node. It uses // aggregate metadata, plus the set of origin node IDs, to produce tables. -func MakeDetailedNode(r report.Report, n RenderableNode) DetailedNode { +func MakeDetailedNode(r report.Report, n RenderableNode, addHostTags bool) DetailedNode { tables := tables{} // RenderableNode may be the result of merge operation(s), and so may have // multiple origins. The ultimate goal here is to generate tables to view @@ -80,7 +80,7 @@ func MakeDetailedNode(r report.Report, n RenderableNode) DetailedNode { // add them later. connections := []Row{} for _, id := range n.Origins { - if table, ok := OriginTable(r, id); ok { + if table, ok := OriginTable(r, id, addHostTags); ok { tables = append(tables, table) } else if _, ok := r.Endpoint.NodeMetadatas[id]; ok { connections = append(connections, connectionDetailsRows(r.Endpoint, id)...) @@ -155,12 +155,12 @@ func addConnectionsTable(tables *tables, connections []Row, r report.Report, n R // OriginTable produces a table (to be consumed directly by the UI) based on // an origin ID, which is (optimistically) a node ID in one of our topologies. -func OriginTable(r report.Report, originID string) (Table, bool) { +func OriginTable(r report.Report, originID string, addHostTags bool) (Table, bool) { if nmd, ok := r.Process.NodeMetadatas[originID]; ok { - return processOriginTable(nmd) + return processOriginTable(nmd, addHostTags) } if nmd, ok := r.Container.NodeMetadatas[originID]; ok { - return containerOriginTable(nmd) + return containerOriginTable(nmd, addHostTags) } if nmd, ok := r.ContainerImage.NodeMetadatas[originID]; ok { return containerImageOriginTable(nmd) @@ -224,7 +224,7 @@ func connectionDetailsRows(topology report.Topology, originID string) []Row { return rows } -func processOriginTable(nmd report.NodeMetadata) (Table, bool) { +func processOriginTable(nmd report.NodeMetadata, addHostTag bool) (Table, bool) { rows := []Row{} for _, tuple := range []struct{ key, human string }{ {process.Comm, "Name"}, @@ -237,7 +237,9 @@ func processOriginTable(nmd report.NodeMetadata) (Table, bool) { rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""}) } } - + if addHostTag { + rows = append([]Row{{Key: "Host", ValueMajor: report.ExtractHostID(nmd)}}, rows...) + } return Table{ Title: "Origin Process", Numeric: false, @@ -246,7 +248,7 @@ func processOriginTable(nmd report.NodeMetadata) (Table, bool) { }, len(rows) > 0 } -func containerOriginTable(nmd report.NodeMetadata) (Table, bool) { +func containerOriginTable(nmd report.NodeMetadata, addHostTag bool) (Table, bool) { rows := []Row{} for _, tuple := range []struct{ key, human string }{ {docker.ContainerID, "ID"}, @@ -268,7 +270,9 @@ func containerOriginTable(nmd report.NodeMetadata) (Table, bool) { rows = append(rows, Row{Key: "Memory Usage (MB):", ValueMajor: memoryStr, ValueMinor: ""}) } } - + if addHostTag { + rows = append([]Row{{Key: "Host", ValueMajor: report.ExtractHostID(nmd)}}, rows...) + } return Table{ Title: "Origin Container", Numeric: false, diff --git a/render/detailed_node_test.go b/render/detailed_node_test.go index 11742d62e..4cba8359a 100644 --- a/render/detailed_node_test.go +++ b/render/detailed_node_test.go @@ -10,7 +10,7 @@ import ( ) func TestOriginTable(t *testing.T) { - if _, ok := render.OriginTable(test.Report, "not-found"); ok { + if _, ok := render.OriginTable(test.Report, "not-found", false); ok { t.Errorf("unknown origin ID gave unexpected success") } for originID, want := range map[string]render.Table{ @@ -34,7 +34,7 @@ func TestOriginTable(t *testing.T) { }, }, } { - have, ok := render.OriginTable(test.Report, originID) + have, ok := render.OriginTable(test.Report, originID, false) if !ok { t.Errorf("%q: not OK", originID) continue @@ -43,11 +43,46 @@ func TestOriginTable(t *testing.T) { t.Errorf("%q: %s", originID, test.Diff(want, have)) } } + + // Test host tags + for originID, want := range map[string]render.Table{ + test.ServerProcessNodeID: { + Title: "Origin Process", + Numeric: false, + Rank: 2, + Rows: []render.Row{ + {"Host", test.ServerHostID, "", false}, + {"Name", "apache", "", false}, + {"PID", test.ServerPID, "", false}, + }, + }, + test.ServerContainerNodeID: { + Title: "Origin Container", + Numeric: false, + Rank: 3, + Rows: []render.Row{ + {"Host", test.ServerHostID, "", false}, + {"ID", test.ServerContainerID, "", false}, + {"Name", "server", "", false}, + {"Image ID", test.ServerContainerImageID, "", false}, + }, + }, + } { + have, ok := render.OriginTable(test.Report, originID, true) + if !ok { + t.Errorf("%q: not OK", originID) + continue + } + if !reflect.DeepEqual(want, have) { + t.Errorf("%q: %s", originID, test.Diff(want, have)) + } + } + } func TestMakeDetailedHostNode(t *testing.T) { renderableNode := render.HostRenderer.Render(test.Report)[render.MakeHostID(test.ClientHostID)] - have := render.MakeDetailedNode(test.Report, renderableNode) + have := render.MakeDetailedNode(test.Report, renderableNode, false) want := render.DetailedNode{ ID: render.MakeHostID(test.ClientHostID), LabelMajor: "client", @@ -109,7 +144,7 @@ func TestMakeDetailedHostNode(t *testing.T) { func TestMakeDetailedContainerNode(t *testing.T) { renderableNode := render.ContainerRenderer.Render(test.Report)[test.ServerContainerID] - have := render.MakeDetailedNode(test.Report, renderableNode) + have := render.MakeDetailedNode(test.Report, renderableNode, false) want := render.DetailedNode{ ID: test.ServerContainerID, LabelMajor: "server",