Produce the container topology by way of the process topology.

This commit is contained in:
Tom Wilkie
2015-06-16 16:31:15 +00:00
parent a759db8931
commit 16e2ccd2be
9 changed files with 212 additions and 108 deletions

View File

@@ -58,12 +58,9 @@ var topologyRegistry = map[string]topologyView{
renderer: render.LeafMap{Selector: report.SelectEndpoint, Mapper: render.ProcessName, Pseudo: render.GenericGroupedPseudoNode},
},
"containers": {
human: "Containers",
parent: "",
renderer: render.Reduce([]render.Renderer{
render.LeafMap{Selector: report.SelectEndpoint, Mapper: render.MapEndpoint2Container, Pseudo: render.InternetOnlyPseudoNode},
render.LeafMap{Selector: report.SelectContainer, Mapper: render.MapContainerIdentity, Pseudo: render.InternetOnlyPseudoNode},
}),
human: "Containers",
parent: "",
renderer: render.ContainerRenderer,
},
"containers-by-image": {
human: "by image",

View File

@@ -76,7 +76,11 @@ func addConnection(
if _, ok := r.Endpoint.NodeMetadatas[scopedLocal]; !ok {
// First hit establishes NodeMetadata for scoped local address + port
md := report.NodeMetadata{
"pid": fmt.Sprintf("%d", c.Proc.PID),
"addr": c.LocalAddress.String(),
"port": strconv.Itoa(int(c.LocalPort)),
"pid": fmt.Sprintf("%d", c.Proc.PID),
// TODO: These can go away once we derives process graph from process topology
"name": c.Proc.Name,
"domain": hostID,
}

View File

@@ -7,30 +7,12 @@ import (
"github.com/weaveworks/scope/report"
)
const humanTheInternet = "the Internet"
const (
uncontainedID = "uncontained"
uncontainedMajor = "Uncontained"
// NewRenderableNode makes a new RenderableNode
func NewRenderableNode(id, major, minor, rank string) RenderableNode {
return RenderableNode{
ID: id,
LabelMajor: major,
LabelMinor: minor,
Rank: rank,
Pseudo: false,
Metadata: report.AggregateMetadata{},
}
}
func newPseudoNode(id, major, minor string) RenderableNode {
return RenderableNode{
ID: id,
LabelMajor: major,
LabelMinor: minor,
Rank: "",
Pseudo: true,
Metadata: report.AggregateMetadata{},
}
}
humanTheInternet = "the Internet"
)
// LeafMapFunc is anything which can take an arbitrary NodeMetadata, which is
// always one-to-one with nodes in a topology, and return a specific
@@ -54,6 +36,84 @@ type PseudoFunc func(srcNodeID string, srcNode RenderableNode, dstNodeID string)
// return another RenderableNode.
type MapFunc func(RenderableNode) (RenderableNode, bool)
// MapEndpointIdentity maps a endpoint topology node to endpoint RenderableNode node.
// As it is only ever run on endpoint topology nodes, we can safely assume the
// presences of certain keys.
func MapEndpointIdentity(m report.NodeMetadata) (RenderableNode, bool) {
var (
id = fmt.Sprintf("endpoint:%s:%s:%s", m[report.HostNodeID], m["addr"], m["port"])
major = fmt.Sprintf("%s:%s", m["addr"], m["port"])
minor = fmt.Sprintf("%s (%s)", m[report.HostNodeID], m["pid"])
rank = m["pid"]
)
return NewRenderableNode(id, major, minor, rank, m), true
}
// MapProcessIdentity maps a process topology node to process RenderableNode node.
// As it is only ever run on process topology nodes, we can safely assume the
// presences of certain keys.
func MapProcessIdentity(m report.NodeMetadata) (RenderableNode, bool) {
var (
id = fmt.Sprintf("pid:%s:%s", m[report.HostNodeID], m["pid"])
major = m["comm"]
minor = fmt.Sprintf("%s (%s)", m[report.HostNodeID], m["pid"])
rank = m["pid"]
)
return NewRenderableNode(id, major, minor, rank, m), true
}
// MapContainerIdentity maps a container topology node to container RenderableNode node.
// As it is only ever run on container topology nodes, we can safely assume the
// presences of certain keys.
func MapContainerIdentity(m report.NodeMetadata) (RenderableNode, bool) {
var (
id = m["docker_container_id"]
major = m["docker_container_name"]
minor = m[report.HostNodeID]
rank = m["docker_image_id"]
)
return NewRenderableNode(id, major, minor, rank, m), true
}
// MapEndpoint2Process maps endpoint RenderableNodes to process RenderableNodes.
//
// If this function is given a pseudo node, then it will just return it;
// Pseudo nodes will never have pids in them, and therefore will never
// be able to be turned into a Process node.
//
// Otherwise, this function will produce a node with the correct ID
// format for a process, but without any Major or Minor labels.
// It does not have enough info to do that, and the resulting graph
// must be merged with a process graph to get that info.
func MapEndpoint2Process(n RenderableNode) (RenderableNode, bool) {
if n.Pseudo {
return n, true
}
id := fmt.Sprintf("pid:%s:%s", n.NodeMetadata[report.HostNodeID], n.NodeMetadata["pid"])
return newDerivedNode(id, n), true
}
// MapProcess2Container maps process RenderableNodes to container RenderableNodes.
//
// If this function is given a node without a docker_container_id (including other
// psueod nodes), it will produce an "Uncontained" pseudo node.
//
// Otherwise, this function will produce a node with the correct ID
// format for a container, but without any Major or Minor labels.
// It does not have enough info to do that, and the resulting graph
// must be merged with a container graph to get that info.
func MapProcess2Container(n RenderableNode) (RenderableNode, bool) {
id, ok := n.NodeMetadata["docker_container_id"]
if !ok {
return newPseudoNode(uncontainedID, uncontainedMajor, ""), true
}
return newDerivedNode(id, n), true
}
// ProcessPID takes a node NodeMetadata from topology, and returns a
// representation with the ID based on the process PID and the labels based on
// the process name.
@@ -64,7 +124,7 @@ func ProcessPID(m report.NodeMetadata) (RenderableNode, bool) {
show = m["pid"] != "" && m["name"] != ""
)
return NewRenderableNode(identifier, m["name"], minor, m["pid"]), show
return NewRenderableNode(identifier, m["name"], minor, m["pid"], m), show
}
// ProcessName takes a node NodeMetadata from a topology, and returns a
@@ -72,34 +132,7 @@ func ProcessPID(m report.NodeMetadata) (RenderableNode, bool) {
// processes with the same name together).
func ProcessName(m report.NodeMetadata) (RenderableNode, bool) {
show := m["pid"] != "" && m["name"] != ""
return NewRenderableNode(m["name"], m["name"], "", m["name"]), show
}
// MapEndpoint2Container maps endpoint topology nodes to the containers they run
// in. We consider container and image IDs to be globally unique, and so don't
// scope them further by e.g. host. If no container metadata is found, nodes are
// grouped into the Uncontained node.
func MapEndpoint2Container(m report.NodeMetadata) (RenderableNode, bool) {
var id, major, minor, rank string
if m["docker_container_id"] == "" {
id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained"
} else {
id, major, minor, rank = m["docker_container_id"], "", m["domain"], ""
}
return NewRenderableNode(id, major, minor, rank), true
}
// MapContainerIdentity maps container topology node to container mapped nodes.
func MapContainerIdentity(m report.NodeMetadata) (RenderableNode, bool) {
var id, major, minor, rank string
if m["docker_container_id"] == "" {
id, major, minor, rank = "uncontained", "Uncontained", "", "uncontained"
} else {
id, major, minor, rank = m["docker_container_id"], m["docker_container_name"], m["domain"], m["docker_image_id"]
}
return NewRenderableNode(id, major, minor, rank), true
return NewRenderableNode(m["name"], m["name"], "", m["name"], m), show
}
// ProcessContainerImage maps topology nodes to the container images they run
@@ -113,7 +146,7 @@ func ProcessContainerImage(m report.NodeMetadata) (RenderableNode, bool) {
id, major, minor, rank = m["docker_image_id"], m["docker_image_name"], "", m["docker_image_id"]
}
return NewRenderableNode(id, major, minor, rank), true
return NewRenderableNode(id, major, minor, rank, m), true
}
// NetworkHostname takes a node NodeMetadata and returns a representation
@@ -130,7 +163,7 @@ func NetworkHostname(m report.NodeMetadata) (RenderableNode, bool) {
domain = parts[1]
}
return NewRenderableNode(fmt.Sprintf("host:%s", name), parts[0], domain, parts[0]), name != ""
return NewRenderableNode(fmt.Sprintf("host:%s", name), parts[0], domain, parts[0], m), name != ""
}
// GenericPseudoNode contains heuristics for building sensible pseudo nodes.

View File

@@ -54,38 +54,6 @@ func TestUngroupedMapping(t *testing.T) {
wantMinor: "hosta (42)",
wantRank: "42",
},
{
f: render.MapEndpoint2Container,
id: "foo-id",
meta: report.NodeMetadata{
"pid": "42",
"name": "curl",
"domain": "hosta",
},
wantOK: true,
wantID: "uncontained",
wantMajor: "Uncontained",
wantMinor: "",
wantRank: "uncontained",
},
{
f: render.MapEndpoint2Container,
id: "bar-id",
meta: report.NodeMetadata{
"pid": "42",
"name": "curl",
"domain": "hosta",
"docker_container_id": "d321fe0",
"docker_container_name": "walking_sparrow",
"docker_image_id": "1101fff",
"docker_image_name": "org/app:latest",
},
wantOK: true,
wantID: "d321fe0",
wantMajor: "",
wantMinor: "hosta",
wantRank: "",
},
} {
identity := fmt.Sprintf("(%d %s %v)", i, c.id, c.meta)

View File

@@ -31,6 +31,11 @@ type LeafMap struct {
Pseudo PseudoFunc
}
// MakeReduce is the only sane way to produce a Reduce Renderer
func MakeReduce(renderers ...Renderer) Renderer {
return Reduce(renderers)
}
// Render produces a set of RenderableNodes given a Report
func (r Reduce) Render(rpt report.Report) RenderableNodes {
result := RenderableNodes{}

View File

@@ -134,7 +134,7 @@ func TestMapEdge(t *testing.T) {
}
identity := func(nmd report.NodeMetadata) (render.RenderableNode, bool) {
return render.NewRenderableNode(nmd["id"], "", "", ""), true
return render.NewRenderableNode(nmd["id"], "", "", "", nmd), true
}
mapper := render.Map{
@@ -331,6 +331,15 @@ var (
}
)
func trimNodeMetadata(rns render.RenderableNodes) render.RenderableNodes {
result := render.RenderableNodes{}
for id, rn := range rns {
rn.NodeMetadata = nil
result[id] = rn
}
return result
}
func TestRenderByEndpointPID(t *testing.T) {
want := render.RenderableNodes{
"pid:client-54001-domain:10001": {
@@ -392,9 +401,10 @@ func TestRenderByEndpointPID(t *testing.T) {
}
have := render.LeafMap{
Selector: report.SelectEndpoint,
Mapper: render.ProcessPID,
Pseudo: render.GenericPseudoNode,
Mapper: render.ProcessPID,
Pseudo: render.GenericPseudoNode,
}.Render(rpt)
have = trimNodeMetadata(have)
if !reflect.DeepEqual(want, have) {
t.Error("\n" + diff(want, have))
}
@@ -450,9 +460,10 @@ func TestRenderByEndpointPIDGrouped(t *testing.T) {
}
have := render.LeafMap{
Selector: report.SelectEndpoint,
Mapper: render.ProcessName,
Pseudo: render.GenericGroupedPseudoNode,
Mapper: render.ProcessName,
Pseudo: render.GenericGroupedPseudoNode,
}.Render(rpt)
have = trimNodeMetadata(have)
if !reflect.DeepEqual(want, have) {
t.Error("\n" + diff(want, have))
}
@@ -509,9 +520,10 @@ func TestRenderByNetworkHostname(t *testing.T) {
}
have := render.LeafMap{
Selector: report.SelectAddress,
Mapper: render.NetworkHostname,
Pseudo: render.GenericPseudoNode,
Mapper: render.NetworkHostname,
Pseudo: render.GenericPseudoNode,
}.Render(rpt)
have = trimNodeMetadata(have)
if !reflect.DeepEqual(want, have) {
t.Error("\n" + diff(want, have))
}

View File

@@ -8,14 +8,15 @@ import (
// an element of a topology. It should contain information that's relevant
// to rendering a node when there are many nodes visible at once.
type RenderableNode struct {
ID string `json:"id"` //
LabelMajor string `json:"label_major"` // e.g. "process", human-readable
LabelMinor string `json:"label_minor,omitempty"` // e.g. "hostname", human-readable, optional
Rank string `json:"rank"` // to help the layout engine
Pseudo bool `json:"pseudo,omitempty"` // sort-of a placeholder node, for rendering purposes
Adjacency report.IDList `json:"adjacency,omitempty"` // Node IDs (in the same topology domain)
Origins report.IDList `json:"origins,omitempty"` // Core node IDs that contributed information
Metadata report.AggregateMetadata `json:"metadata"` // Numeric sums
ID string `json:"id"` //
LabelMajor string `json:"label_major"` // e.g. "process", human-readable
LabelMinor string `json:"label_minor,omitempty"` // e.g. "hostname", human-readable, optional
Rank string `json:"rank"` // to help the layout engine
Pseudo bool `json:"pseudo,omitempty"` // sort-of a placeholder node, for rendering purposes
Adjacency report.IDList `json:"adjacency,omitempty"` // Node IDs (in the same topology domain)
Origins report.IDList `json:"origins,omitempty"` // Core node IDs that contributed information
Metadata report.AggregateMetadata `json:"metadata"` // Numeric sums
NodeMetadata report.NodeMetadata `json:"-"` // merged NodeMetadata of the nodes used to build this
}
// RenderableNodes is a set of RenderableNodes
@@ -55,4 +56,43 @@ func (rn *RenderableNode) Merge(other RenderableNode) {
rn.Origins = rn.Origins.Add(other.Origins...)
rn.Metadata.Merge(other.Metadata)
rn.NodeMetadata.Merge(other.NodeMetadata)
}
// NewRenderableNode makes a new RenderableNode
func NewRenderableNode(id, major, minor, rank string, nmd report.NodeMetadata) RenderableNode {
return RenderableNode{
ID: id,
LabelMajor: major,
LabelMinor: minor,
Rank: rank,
Pseudo: false,
Metadata: report.AggregateMetadata{},
NodeMetadata: nmd,
}
}
func newDerivedNode(id string, node RenderableNode) RenderableNode {
return RenderableNode{
ID: id,
LabelMajor: "",
LabelMinor: "",
Rank: "",
Pseudo: node.Pseudo,
Metadata: node.Metadata,
Origins: node.Origins,
NodeMetadata: node.NodeMetadata,
}
}
func newPseudoNode(id, major, minor string) RenderableNode {
return RenderableNode{
ID: id,
LabelMajor: major,
LabelMinor: minor,
Rank: "",
Pseudo: true,
Metadata: report.AggregateMetadata{},
NodeMetadata: report.NodeMetadata{},
}
}

40
render/topologies.go Normal file
View File

@@ -0,0 +1,40 @@
package render
import (
"github.com/weaveworks/scope/report"
)
// EndpointRenderer is a Renderer which produces a renderable endpoint graph.
var EndpointRenderer = LeafMap{
Selector: report.SelectEndpoint,
Mapper: MapEndpointIdentity,
Pseudo: GenericPseudoNode,
}
// ProcessRenderer is a Renderer which produces a renderable process
// graph by merging the endpoint graph and the process topology.
var ProcessRenderer = MakeReduce(
Map{
MapFunc: MapEndpoint2Process,
Renderer: EndpointRenderer,
},
LeafMap{
Selector: report.SelectProcess,
Mapper: MapProcessIdentity,
Pseudo: GenericPseudoNode,
},
)
// ContainerRenderer is a Renderer which produces a renderable container
// graph by merging the process graph and the container topology.
var ContainerRenderer = MakeReduce(
Map{
MapFunc: MapProcess2Container,
Renderer: ProcessRenderer,
},
LeafMap{
Selector: report.SelectContainer,
Mapper: MapContainerIdentity,
Pseudo: GenericPseudoNode,
},
)

View File

@@ -53,6 +53,11 @@ func SelectEndpoint(r Report) Topology {
return r.Endpoint
}
// SelectProcess selects the process topology.
func SelectProcess(r Report) Topology {
return r.Process
}
// SelectAddress selects the address topology.
func SelectAddress(r Report) Topology {
return r.Address