Give mapping functions the ability to return multiple nodes.

This commit is contained in:
Tom Wilkie
2015-08-24 12:07:55 +00:00
parent 81eb7fafc4
commit e4970f9214
4 changed files with 142 additions and 133 deletions

View File

@@ -31,10 +31,7 @@ const (
//
// A single NodeMetadata can yield arbitrary many representations, including
// representations that reduce the cardinality of the set of nodes.
//
// If the final output parameter is false, the node shall be omitted from the
// rendered topology.
type LeafMapFunc func(report.NodeMetadata) (RenderableNode, bool)
type LeafMapFunc func(report.NodeMetadata) RenderableNodes
// PseudoFunc creates RenderableNode representing pseudo nodes given the
// srcNodeID. dstNodeID is the node id of one of the nodes this node has an
@@ -48,20 +45,20 @@ type PseudoFunc func(srcNodeID, dstNodeID string, srcIsClient bool, local report
//
// As with LeafMapFunc, if the final output parameter is false, the node
// shall be omitted from the rendered topology.
type MapFunc func(RenderableNode) (RenderableNode, bool)
type MapFunc func(RenderableNode) RenderableNodes
// MapEndpointIdentity maps an endpoint topology node to an endpoint
// renderable node. As it is only ever run on endpoint topology nodes, we
// expect that certain keys are present.
func MapEndpointIdentity(m report.NodeMetadata) (RenderableNode, bool) {
func MapEndpointIdentity(m report.NodeMetadata) RenderableNodes {
addr, ok := m.Metadata[endpoint.Addr]
if !ok {
return RenderableNode{}, false
return RenderableNodes{}
}
port, ok := m.Metadata[endpoint.Port]
if !ok {
return RenderableNode{}, false
return RenderableNodes{}
}
var (
@@ -75,16 +72,16 @@ func MapEndpointIdentity(m report.NodeMetadata) (RenderableNode, bool) {
minor = fmt.Sprintf("%s (%s)", minor, pid)
}
return NewRenderableNode(id, major, minor, rank, m), true
return RenderableNodes{id: NewRenderableNode(id, major, minor, rank, m)}
}
// MapProcessIdentity maps a process topology node to a process renderable
// node. As it is only ever run on process topology nodes, we expect that
// certain keys are present.
func MapProcessIdentity(m report.NodeMetadata) (RenderableNode, bool) {
func MapProcessIdentity(m report.NodeMetadata) RenderableNodes {
pid, ok := m.Metadata[process.PID]
if !ok {
return RenderableNode{}, false
return RenderableNodes{}
}
var (
@@ -94,16 +91,16 @@ func MapProcessIdentity(m report.NodeMetadata) (RenderableNode, bool) {
rank = m.Metadata["comm"]
)
return NewRenderableNode(id, major, minor, rank, m), true
return RenderableNodes{id: NewRenderableNode(id, major, minor, rank, m)}
}
// MapContainerIdentity maps a container topology node to a container
// renderable node. As it is only ever run on container topology nodes, we
// expect that certain keys are present.
func MapContainerIdentity(m report.NodeMetadata) (RenderableNode, bool) {
func MapContainerIdentity(m report.NodeMetadata) RenderableNodes {
id, ok := m.Metadata[docker.ContainerID]
if !ok {
return RenderableNode{}, false
return RenderableNodes{}
}
var (
@@ -112,16 +109,16 @@ func MapContainerIdentity(m report.NodeMetadata) (RenderableNode, bool) {
rank = m.Metadata[docker.ImageID]
)
return NewRenderableNode(id, major, minor, rank, m), true
return RenderableNodes{id: NewRenderableNode(id, major, minor, rank, m)}
}
// MapContainerImageIdentity maps a container image topology node to container
// image renderable node. As it is only ever run on container image topology
// nodes, we expect that certain keys are present.
func MapContainerImageIdentity(m report.NodeMetadata) (RenderableNode, bool) {
func MapContainerImageIdentity(m report.NodeMetadata) RenderableNodes {
id, ok := m.Metadata[docker.ImageID]
if !ok {
return RenderableNode{}, false
return RenderableNodes{}
}
var (
@@ -129,16 +126,16 @@ func MapContainerImageIdentity(m report.NodeMetadata) (RenderableNode, bool) {
rank = m.Metadata[docker.ImageID]
)
return NewRenderableNode(id, major, "", rank, m), true
return RenderableNodes{id: NewRenderableNode(id, major, "", rank, m)}
}
// MapAddressIdentity maps an address topology node to an address renderable
// node. As it is only ever run on address topology nodes, we expect that
// certain keys are present.
func MapAddressIdentity(m report.NodeMetadata) (RenderableNode, bool) {
func MapAddressIdentity(m report.NodeMetadata) RenderableNodes {
addr, ok := m.Metadata[endpoint.Addr]
if !ok {
return RenderableNode{}, false
return RenderableNodes{}
}
var (
@@ -148,13 +145,13 @@ func MapAddressIdentity(m report.NodeMetadata) (RenderableNode, bool) {
rank = major
)
return NewRenderableNode(id, major, minor, rank, m), true
return RenderableNodes{id: NewRenderableNode(id, major, minor, rank, m)}
}
// MapHostIdentity maps a host topology node to a host renderable node. As it
// is only ever run on host topology nodes, we expect that certain keys are
// present.
func MapHostIdentity(m report.NodeMetadata) (RenderableNode, bool) {
func MapHostIdentity(m report.NodeMetadata) RenderableNodes {
var (
id = MakeHostID(report.ExtractHostID(m))
hostname = m.Metadata[host.HostName]
@@ -168,7 +165,7 @@ func MapHostIdentity(m report.NodeMetadata) (RenderableNode, bool) {
major = hostname
}
return NewRenderableNode(id, major, minor, rank, m), true
return RenderableNodes{id: NewRenderableNode(id, major, minor, rank, m)}
}
// MapEndpoint2Process maps endpoint RenderableNodes to process
@@ -182,18 +179,18 @@ func MapHostIdentity(m report.NodeMetadata) (RenderableNode, bool) {
// 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) {
func MapEndpoint2Process(n RenderableNode) RenderableNodes {
if n.Pseudo {
return n, true
return RenderableNodes{n.ID: n}
}
pid, ok := n.NodeMetadata.Metadata[process.PID]
if !ok {
return RenderableNode{}, false
return RenderableNodes{}
}
id := MakeProcessID(report.ExtractHostID(n.NodeMetadata), pid)
return newDerivedNode(id, n), true
return RenderableNodes{id: newDerivedNode(id, n)}
}
// MapProcess2Container maps process RenderableNodes to container
@@ -207,15 +204,15 @@ func MapEndpoint2Process(n RenderableNode) (RenderableNode, bool) {
// 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) {
func MapProcess2Container(n RenderableNode) RenderableNodes {
// Propogate the internet pseudo node
if n.ID == TheInternetID {
return n, true
return RenderableNodes{n.ID: n}
}
// Don't propogate non-internet pseudo nodes
if n.Pseudo {
return n, false
return RenderableNodes{}
}
// Otherwise, if the process is not in a container, group it
@@ -228,10 +225,10 @@ func MapProcess2Container(n RenderableNode) (RenderableNode, bool) {
id = MakePseudoNodeID(UncontainedID, hostID)
node := newDerivedPseudoNode(id, UncontainedMajor, n)
node.LabelMinor = hostID
return node, true
return RenderableNodes{id: node}
}
return newDerivedNode(id, n), true
return RenderableNodes{id: newDerivedNode(id, n)}
}
// MapProcess2Name maps process RenderableNodes to RenderableNodes
@@ -240,29 +237,29 @@ func MapProcess2Container(n RenderableNode) (RenderableNode, bool) {
// This mapper is unlike the other foo2bar mappers as the intention
// is not to join the information with another topology. Therefore
// it outputs a properly-formed node with labels etc.
func MapProcess2Name(n RenderableNode) (RenderableNode, bool) {
func MapProcess2Name(n RenderableNode) RenderableNodes {
if n.Pseudo {
return n, true
return RenderableNodes{n.ID: n}
}
name, ok := n.NodeMetadata.Metadata["comm"]
if !ok {
return RenderableNode{}, false
return RenderableNodes{}
}
node := newDerivedNode(name, n)
node.LabelMajor = name
node.Rank = name
node.NodeMetadata.Counters[processesKey] = 1
return node, true
return RenderableNodes{name: node}
}
// MapCountProcessName maps 1:1 process name nodes, counting
// the number of processes grouped together and putting
// that info in the minor label.
func MapCountProcessName(n RenderableNode) (RenderableNode, bool) {
func MapCountProcessName(n RenderableNode) RenderableNodes {
if n.Pseudo {
return n, true
return RenderableNodes{n.ID: n}
}
processes := n.NodeMetadata.Counters[processesKey]
@@ -271,7 +268,7 @@ func MapCountProcessName(n RenderableNode) (RenderableNode, bool) {
} else {
n.LabelMinor = fmt.Sprintf("%d processes", processes)
}
return n, true
return RenderableNodes{n.ID: n}
}
// MapContainer2ContainerImage maps container RenderableNodes to container
@@ -285,23 +282,23 @@ func MapCountProcessName(n RenderableNode) (RenderableNode, bool) {
// 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 MapContainer2ContainerImage(n RenderableNode) (RenderableNode, bool) {
func MapContainer2ContainerImage(n RenderableNode) RenderableNodes {
// Propogate all pseudo nodes
if n.Pseudo {
return n, true
return RenderableNodes{n.ID: n}
}
// Otherwise, if some some reason the container doesn't have a image_id
// (maybe slightly out of sync reports), just drop it
id, ok := n.NodeMetadata.Metadata[docker.ImageID]
if !ok {
return n, false
return RenderableNodes{}
}
// Add container-<id> key to NMD, which will later be counted to produce the minor label
result := newDerivedNode(id, n)
result.NodeMetadata.Counters[containersKey] = 1
return result, true
return RenderableNodes{id: result}
}
// MapContainerImage2Name maps container images RenderableNodes to
@@ -310,14 +307,14 @@ func MapContainer2ContainerImage(n RenderableNode) (RenderableNode, bool) {
// This mapper is unlike the other foo2bar mappers as the intention
// is not to join the information with another topology. Therefore
// it outputs a properly-formed node with labels etc.
func MapContainerImage2Name(n RenderableNode) (RenderableNode, bool) {
func MapContainerImage2Name(n RenderableNode) RenderableNodes {
if n.Pseudo {
return n, true
return RenderableNodes{n.ID: n}
}
name, ok := n.NodeMetadata.Metadata[docker.ImageName]
if !ok {
return RenderableNode{}, false
return RenderableNodes{}
}
parts := strings.SplitN(name, ":", 2)
@@ -329,15 +326,15 @@ func MapContainerImage2Name(n RenderableNode) (RenderableNode, bool) {
node.LabelMajor = name
node.Rank = name
node.NodeMetadata = n.NodeMetadata.Copy() // Propagate NMD for container counting.
return node, true
return RenderableNodes{name: node}
}
// MapCountContainers maps 1:1 container image nodes, counting
// the number of containers grouped together and putting
// that info in the minor label.
func MapCountContainers(n RenderableNode) (RenderableNode, bool) {
func MapCountContainers(n RenderableNode) RenderableNodes {
if n.Pseudo {
return n, true
return RenderableNodes{n.ID: n}
}
containers := n.NodeMetadata.Counters[containersKey]
@@ -346,19 +343,19 @@ func MapCountContainers(n RenderableNode) (RenderableNode, bool) {
} else {
n.LabelMinor = fmt.Sprintf("%d containers", containers)
}
return n, true
return RenderableNodes{n.ID: n}
}
// MapAddress2Host maps address RenderableNodes to host RenderableNodes.
//
// Otherthan pseudo nodes, we can assume all nodes have a HostID
func MapAddress2Host(n RenderableNode) (RenderableNode, bool) {
func MapAddress2Host(n RenderableNode) RenderableNodes {
if n.Pseudo {
return n, true
return RenderableNodes{n.ID: n}
}
id := MakeHostID(report.ExtractHostID(n.NodeMetadata))
return newDerivedNode(id, n), true
return RenderableNodes{id: newDerivedNode(id, n)}
}
// GenericPseudoNode makes a PseudoFunc given an addresser. The returned

View File

@@ -72,7 +72,7 @@ type testcase struct {
}
func testMap(t *testing.T, f render.LeafMapFunc, input testcase) {
if _, have := f(input.md); input.ok != have {
if have := f(input.md); input.ok != (len(have) > 0) {
t.Errorf("%v: want %v, have %v", input.md, input.ok, have)
}
}

View File

@@ -53,26 +53,24 @@ func (m Map) Render(rpt report.Report) RenderableNodes {
return output
}
func (m Map) render(rpt report.Report) (RenderableNodes, map[string]string) {
func (m Map) render(rpt report.Report) (RenderableNodes, map[string]report.IDList) {
input := m.Renderer.Render(rpt)
output := RenderableNodes{}
mapped := map[string]string{} // input node ID -> output node ID
mapped := map[string]report.IDList{} // input node ID -> output node IDs
adjacencies := map[string]report.IDList{} // output node ID -> input node Adjacencies
for _, inRenderable := range input {
outRenderable, ok := m.MapFunc(inRenderable)
if !ok {
continue
}
outRenderables := m.MapFunc(inRenderable)
for _, outRenderable := range outRenderables {
existing, ok := output[outRenderable.ID]
if ok {
outRenderable.Merge(existing)
}
existing, ok := output[outRenderable.ID]
if ok {
outRenderable.Merge(existing)
output[outRenderable.ID] = outRenderable
mapped[inRenderable.ID] = mapped[inRenderable.ID].Add(outRenderable.ID)
adjacencies[outRenderable.ID] = adjacencies[outRenderable.ID].Merge(inRenderable.Adjacency)
}
output[outRenderable.ID] = outRenderable
mapped[inRenderable.ID] = outRenderable.ID
adjacencies[outRenderable.ID] = adjacencies[outRenderable.ID].Merge(inRenderable.Adjacency)
}
// Rewrite Adjacency for new node IDs.
@@ -82,7 +80,7 @@ func (m Map) render(rpt report.Report) (RenderableNodes, map[string]string) {
for outNodeID, inAdjacency := range adjacencies {
outAdjacency := report.MakeIDList()
for _, inAdjacent := range inAdjacency {
if outAdjacent, ok := mapped[inAdjacent]; ok {
for _, outAdjacent := range mapped[inAdjacent] {
outAdjacency = outAdjacency.Add(outAdjacent)
}
}
@@ -102,10 +100,12 @@ func (m Map) EdgeMetadata(rpt report.Report, srcRenderableID, dstRenderableID st
// First we need to map the ids in this layer into the ids in the underlying layer
_, mapped := m.render(rpt) // this maps from old -> new
inverted := map[string][]string{} // this maps from new -> old(s)
for k, v := range mapped {
existing := inverted[v]
existing = append(existing, k)
inverted[v] = existing
for k, vs := range mapped {
for _, v := range vs {
existing := inverted[v]
existing = append(existing, k)
inverted[v] = existing
}
}
// Now work out a slice of edges this edge is constructed from
@@ -148,34 +148,31 @@ func (m LeafMap) Render(rpt report.Report) RenderableNodes {
// Build a set of RenderableNodes for all non-pseudo probes, and an
// addressID to nodeID lookup map. Multiple addressIDs can map to the same
// RenderableNodes.
source2mapped := map[string]string{} // source node ID -> mapped node ID
source2mapped := map[string]report.IDList{} // source node ID -> mapped node IDs
for nodeID, metadata := range t.NodeMetadatas {
mapped, ok := m.Mapper(metadata)
if !ok {
continue
for _, mapped := range m.Mapper(metadata) {
// mapped.ID needs not be unique over all addressIDs. If not, we merge with
// the existing data, on the assumption that the MapFunc returns the same
// data.
existing, ok := nodes[mapped.ID]
if ok {
mapped.Merge(existing)
}
origins := mapped.Origins
origins = origins.Add(nodeID)
origins = origins.Add(metadata.Metadata[report.HostNodeID])
mapped.Origins = origins
nodes[mapped.ID] = mapped
source2mapped[nodeID] = source2mapped[nodeID].Add(mapped.ID)
}
// mapped.ID needs not be unique over all addressIDs. If not, we merge with
// the existing data, on the assumption that the MapFunc returns the same
// data.
existing, ok := nodes[mapped.ID]
if ok {
mapped.Merge(existing)
}
origins := mapped.Origins
origins = origins.Add(nodeID)
origins = origins.Add(metadata.Metadata[report.HostNodeID])
mapped.Origins = origins
nodes[mapped.ID] = mapped
source2mapped[nodeID] = mapped.ID
}
mkPseudoNode := func(srcNodeID, dstNodeID string, srcIsClient bool) (string, bool) {
mkPseudoNode := func(srcNodeID, dstNodeID string, srcIsClient bool) report.IDList {
pseudoNode, ok := m.Pseudo(srcNodeID, dstNodeID, srcIsClient, localNetworks)
if !ok {
return "", false
return report.MakeIDList()
}
pseudoNode.Origins = pseudoNode.Origins.Add(srcNodeID)
existing, ok := nodes[pseudoNode.ID]
@@ -184,8 +181,8 @@ func (m LeafMap) Render(rpt report.Report) RenderableNodes {
}
nodes[pseudoNode.ID] = pseudoNode
source2mapped[pseudoNode.ID] = srcNodeID
return pseudoNode.ID, true
source2mapped[pseudoNode.ID] = source2mapped[pseudoNode.ID].Add(srcNodeID)
return report.MakeIDList(pseudoNode.ID)
}
// Walk the graph and make connections.
@@ -196,7 +193,7 @@ func (m LeafMap) Render(rpt report.Report) RenderableNodes {
continue
}
srcRenderableID, ok := source2mapped[srcNodeID]
srcRenderableIDs, ok := source2mapped[srcNodeID]
if !ok {
// One of the entries in dsts must be a non-pseudo node
var existingDstNodeID string
@@ -207,40 +204,52 @@ func (m LeafMap) Render(rpt report.Report) RenderableNodes {
}
}
srcRenderableID, ok = mkPseudoNode(srcNodeID, existingDstNodeID, true)
if !ok {
continue
}
srcRenderableIDs = mkPseudoNode(srcNodeID, existingDstNodeID, true)
}
if len(srcRenderableIDs) == 0 {
continue
}
srcRenderableNode := nodes[srcRenderableID]
for _, dstNodeID := range dsts {
dstRenderableID, ok := source2mapped[dstNodeID]
if !ok {
dstRenderableID, ok = mkPseudoNode(dstNodeID, srcNodeID, false)
for _, srcRenderableID := range srcRenderableIDs {
srcRenderableNode := nodes[srcRenderableID]
for _, dstNodeID := range dsts {
dstRenderableIDs, ok := source2mapped[dstNodeID]
if !ok {
dstRenderableIDs = mkPseudoNode(dstNodeID, srcNodeID, false)
}
if len(dstRenderableIDs) == 0 {
continue
}
}
dstRenderableNode := nodes[dstRenderableID]
for _, dstRenderableID := range dstRenderableIDs {
dstRenderableNode := nodes[dstRenderableID]
srcRenderableNode.Adjacency = srcRenderableNode.Adjacency.Add(dstRenderableID)
srcRenderableNode.Adjacency = srcRenderableNode.Adjacency.Add(dstRenderableID)
// We propagate edge metadata to nodes on both ends of the edges.
// TODO we should 'reverse' one end of the edge meta data - ingress -> egress etc.
if md, ok := t.EdgeMetadatas[report.MakeEdgeID(srcNodeID, dstNodeID)]; ok {
srcRenderableNode.EdgeMetadata = srcRenderableNode.EdgeMetadata.Merge(md)
dstRenderableNode.EdgeMetadata = dstRenderableNode.EdgeMetadata.Merge(md)
nodes[dstRenderableID] = dstRenderableNode
// We propagate edge metadata to nodes on both ends of the edges.
// TODO we should 'reverse' one end of the edge meta data - ingress -> egress etc.
if md, ok := t.EdgeMetadatas[report.MakeEdgeID(srcNodeID, dstNodeID)]; ok {
srcRenderableNode.EdgeMetadata = srcRenderableNode.EdgeMetadata.Merge(md)
dstRenderableNode.EdgeMetadata = dstRenderableNode.EdgeMetadata.Merge(md)
nodes[dstRenderableID] = dstRenderableNode
}
}
}
nodes[srcRenderableID] = srcRenderableNode
}
nodes[srcRenderableID] = srcRenderableNode
}
return nodes
}
func ids(nodes RenderableNodes) report.IDList {
result := report.MakeIDList()
for id := range nodes {
result = result.Add(id)
}
return result
}
// EdgeMetadata gives the metadata of an edge from the perspective of the
// srcRenderableID. Since an edgeID can have multiple edges on the address
// level, it uses the supplied mapping function to translate address IDs to
@@ -254,15 +263,16 @@ func (m LeafMap) EdgeMetadata(rpt report.Report, srcRenderableID, dstRenderableI
log.Printf("bad edge ID %q", edgeID)
continue
}
srcs, dsts := report.MakeIDList(src), report.MakeIDList(dst)
if src != report.TheInternet {
mapped, _ := m.Mapper(t.NodeMetadatas[src])
src = mapped.ID
mapped := m.Mapper(t.NodeMetadatas[src])
srcs = ids(mapped)
}
if dst != report.TheInternet {
mapped, _ := m.Mapper(t.NodeMetadatas[dst])
dst = mapped.ID
mapped := m.Mapper(t.NodeMetadatas[dst])
dsts = ids(mapped)
}
if src == srcRenderableID && dst == dstRenderableID {
if srcs.Contains(srcRenderableID) && dsts.Contains(dstRenderableID) {
metadata = metadata.Flatten(edgeMeta)
}
}

View File

@@ -52,8 +52,8 @@ func TestReduceEdge(t *testing.T) {
func TestMapRender1(t *testing.T) {
// 1. Check when we return false, the node gets filtered out
mapper := render.Map{
MapFunc: func(nodes render.RenderableNode) (render.RenderableNode, bool) {
return render.RenderableNode{}, false
MapFunc: func(nodes render.RenderableNode) render.RenderableNodes {
return render.RenderableNodes{}
},
Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{
"foo": {ID: "foo"},
@@ -69,8 +69,8 @@ func TestMapRender1(t *testing.T) {
func TestMapRender2(t *testing.T) {
// 2. Check we can remap two nodes into one
mapper := render.Map{
MapFunc: func(nodes render.RenderableNode) (render.RenderableNode, bool) {
return render.RenderableNode{ID: "bar"}, true
MapFunc: func(nodes render.RenderableNode) render.RenderableNodes {
return render.RenderableNodes{"bar": render.RenderableNode{ID: "bar"}}
},
Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{
"foo": {ID: "foo"},
@@ -89,8 +89,9 @@ func TestMapRender2(t *testing.T) {
func TestMapRender3(t *testing.T) {
// 3. Check we can remap adjacencies
mapper := render.Map{
MapFunc: func(nodes render.RenderableNode) (render.RenderableNode, bool) {
return render.RenderableNode{ID: "_" + nodes.ID}, true
MapFunc: func(nodes render.RenderableNode) render.RenderableNodes {
id := "_" + nodes.ID
return render.RenderableNodes{id: render.RenderableNode{ID: id}}
},
Renderer: mockRenderer{RenderableNodes: render.RenderableNodes{
"foo": {ID: "foo", Adjacency: report.MakeIDList("baz")},
@@ -125,13 +126,14 @@ func TestMapEdge(t *testing.T) {
}
}
identity := func(nmd report.NodeMetadata) (render.RenderableNode, bool) {
return render.NewRenderableNode(nmd.Metadata["id"], "", "", "", nmd), true
identity := func(nmd report.NodeMetadata) render.RenderableNodes {
return render.RenderableNodes{nmd.Metadata["id"]: render.NewRenderableNode(nmd.Metadata["id"], "", "", "", nmd)}
}
mapper := render.Map{
MapFunc: func(n render.RenderableNode) (render.RenderableNode, bool) {
return render.RenderableNode{ID: "_" + n.ID}, true
MapFunc: func(nodes render.RenderableNode) render.RenderableNodes {
id := "_" + nodes.ID
return render.RenderableNodes{id: render.RenderableNode{ID: id}}
},
Renderer: render.LeafMap{
Selector: selector,