Details panel backend redesign

Megasquish:
  [app] remove unused edge endpoint
  [WIP] refactoring node details api endpoint
  [WIP] plumbing the children through the rendering process
  adding IDList.Remove and StringSet.Remove
  [WIP] working on adding parents to detailed node renderings
  WIP UI components with mock backend data for new details
  grouping children by type
  UI components for node details health and info
  metric formatters for details panel
  Column headers and links for details table
  [WIP] started on rendering node metadata and metrics in the detail view
  DetailedNode.LabelMajor -> DetailedNode.Label
  rendering decent labels for parents of detailed nodes
  render metrics onto the top-level detailed node
  removing dead code
  Links to relatives
  metrics have a Format not Unit
  Show more/less actions for tables and relatives
  adjusted metric formatter
  TopologyTagger should tag k8s topology nodes
  make renderablenode ids more consistent, e.g. container:abcd1234
  working on rendering correct summaries for each node
  adding report.Node.Rank, so that merging is independent of order
  rendering children and parents correctly
  output child renderableNode ids, so we can link to them
  add group field to metrics, so they can be grouped
  Refactored details health items to prepare for grouping
  add metrics to processNodeSummaries
  hide summary section if there is no data for it
  fixing up tests
  moving detailed node rendering into a separate package
  Node ID/Topology are fields not metadata
    - This way I think we don't have to care about Metadata being non-commutative.
    - ID and topology are still non-commutative, as I'm not sure how to sanely
  merge them, but it's possible we don't care.
  host memory usage is a filesize, not a percent
  working on fixing some tests
  adding children to hosts detail panel
    - Had to redo how parents are calculated, so that children wouldn't interfere with it
    - have to have the host at the end because it is non-commutative
  only render links for linkable children (i.e. not unconnected processes)
  resolving TODOs
  fixing up lint errors
  make nil a valid value for render.Children so tests are cleaner
  working on backend tests
  make client handle missing metrics property
  Stop rendering container image nodes with process summaries/parents
  fix parent link to container images
  Calculate parents as a set on report.Node (except k8s)
  refactoring detailed.NodeSummary stuff
  removing RenderableNode.Summary*, we already track it on report.Node
  working on tests
  add Columns field to NodeSummaryGroup
  fixing up render/topologies_test
  fix children links to container images
  get children of hosts rendering right
  working on host renderer tests
  Change container report.Node.ID to a1b2c3;<container>
  The id should be globally unique, so we don't need the host id.
    This lets the kubernetes probe return a container node with the pod id,
    which will get merged into the real containers with other reports. The
    catch is that the kubernetes api doesn't tell us which hostname the
    container is running on, so we can't populate the old-style node ids.
  change terminology of system pods and services
  Fix kubernetes services with no selector
  Fixes handling of kubernetes service, which has no pods
  fix parent links for pods/services
  refactor detailed metadata to include sets and latest data
  fixing up host rendering tests
  fleshing out tests for node metadata and metrics
  don't render container pseudo-nodes as processes
  Update test for id format change.
This commit is contained in:
Paul Bellamy
2016-01-19 16:39:37 +01:00
committed by Simon Howe
parent 359ec29c09
commit 56122dd0cc
51 changed files with 1894 additions and 1184 deletions
+135
View File
@@ -0,0 +1,135 @@
package detailed
import (
"fmt"
"sort"
"strings"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/scope/probe/overlay"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/report"
)
var (
processNodeMetadata = renderMetadata(
meta(process.PID, "PID"),
meta(process.PPID, "Parent PID"),
meta(process.Cmdline, "Command"),
meta(process.Threads, "# Threads"),
)
containerNodeMetadata = renderMetadata(
meta(docker.ContainerID, "ID"),
meta(docker.ImageID, "Image ID"),
ltst(docker.ContainerState, "State"),
sets(docker.ContainerIPs, "IPs"),
sets(docker.ContainerPorts, "Ports"),
meta(docker.ContainerCreated, "Created"),
meta(docker.ContainerCommand, "Command"),
meta(overlay.WeaveMACAddress, "Weave MAC"),
meta(overlay.WeaveDNSHostname, "Weave DNS Hostname"),
getDockerLabelRows,
)
containerImageNodeMetadata = renderMetadata(
meta(docker.ImageID, "Image ID"),
getDockerLabelRows,
)
podNodeMetadata = renderMetadata(
meta(kubernetes.PodID, "ID"),
meta(kubernetes.Namespace, "Namespace"),
meta(kubernetes.PodCreated, "Created"),
)
hostNodeMetadata = renderMetadata(
meta(host.HostName, "Hostname"),
meta(host.OS, "Operating system"),
meta(host.KernelVersion, "Kernel version"),
meta(host.Uptime, "Uptime"),
sets(host.LocalNetworks, "Local Networks"),
)
)
// MetadataRow is a row for the metadata table.
type MetadataRow struct {
ID string `json:"id"`
Label string `json:"label"`
Value string `json:"value"`
}
// Copy returns a value copy of a metadata row.
func (m MetadataRow) Copy() MetadataRow {
return MetadataRow{
ID: m.ID,
Label: m.Label,
Value: m.Value,
}
}
// NodeMetadata 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 NodeMetadata(n report.Node) []MetadataRow {
renderers := map[string]func(report.Node) []MetadataRow{
"process": processNodeMetadata,
"container": containerNodeMetadata,
"container_image": containerImageNodeMetadata,
"pod": podNodeMetadata,
"host": hostNodeMetadata,
}
if renderer, ok := renderers[n.Topology]; ok {
return renderer(n)
}
return nil
}
func renderMetadata(templates ...func(report.Node) []MetadataRow) func(report.Node) []MetadataRow {
return func(nmd report.Node) []MetadataRow {
rows := []MetadataRow{}
for _, template := range templates {
rows = append(rows, template(nmd)...)
}
return rows
}
}
func meta(id, label string) func(report.Node) []MetadataRow {
return func(n report.Node) []MetadataRow {
if val, ok := n.Metadata[id]; ok {
return []MetadataRow{{ID: id, Label: label, Value: val}}
}
return nil
}
}
func sets(id, label string) func(report.Node) []MetadataRow {
return func(n report.Node) []MetadataRow {
if val, ok := n.Sets[id]; ok && len(val) > 0 {
return []MetadataRow{{ID: id, Label: label, Value: strings.Join(val, ", ")}}
}
return nil
}
}
func ltst(id, label string) func(report.Node) []MetadataRow {
return func(n report.Node) []MetadataRow {
if val, ok := n.Latest.Lookup(id); ok {
return []MetadataRow{{ID: id, Label: label, Value: val}}
}
return nil
}
}
func getDockerLabelRows(nmd report.Node) []MetadataRow {
rows := []MetadataRow{}
// Add labels in alphabetical order
labels := docker.ExtractLabels(nmd)
labelKeys := make([]string, 0, len(labels))
for k := range labels {
labelKeys = append(labelKeys, k)
}
sort.Strings(labelKeys)
for _, labelKey := range labelKeys {
rows = append(rows, MetadataRow{ID: "label_" + labelKey, Label: fmt.Sprintf("Label %q", labelKey), Value: labels[labelKey]})
}
return rows
}
+53
View File
@@ -0,0 +1,53 @@
package detailed_test
import (
"reflect"
"testing"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/render/detailed"
"github.com/weaveworks/scope/report"
"github.com/weaveworks/scope/test"
"github.com/weaveworks/scope/test/fixture"
)
func TestNodeMetadata(t *testing.T) {
inputs := []struct {
name string
node report.Node
want []detailed.MetadataRow
}{
{
name: "container",
node: report.MakeNodeWith(map[string]string{
docker.ContainerID: fixture.ClientContainerID,
docker.LabelPrefix + "label1": "label1value",
}).WithTopology("container").WithSets(report.Sets{
docker.ContainerIPs: report.MakeStringSet("10.10.10.0/24", "10.10.10.1/24"),
}).WithLatest(docker.ContainerState, fixture.Now, docker.StateRunning),
want: []detailed.MetadataRow{
{ID: docker.ContainerID, Label: "ID", Value: fixture.ClientContainerID},
{ID: docker.ContainerState, Label: "State", Value: "running"},
{ID: docker.ContainerIPs, Label: "IPs", Value: "10.10.10.0/24, 10.10.10.1/24"},
{
ID: "label_label1",
Label: "Label \"label1\"",
Value: "label1value",
},
},
},
{
name: "unknown topology",
node: report.MakeNodeWith(map[string]string{
docker.ContainerID: fixture.ClientContainerID,
}).WithTopology("foobar").WithID(fixture.ClientContainerNodeID),
want: nil,
},
}
for _, input := range inputs {
have := detailed.NodeMetadata(input.node)
if !reflect.DeepEqual(input.want, have) {
t.Errorf("%s: %s", input.name, test.Diff(input.want, have))
}
}
}
+176
View File
@@ -0,0 +1,176 @@
package detailed
import (
"encoding/json"
"math"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/report"
)
const (
defaultFormat = ""
filesizeFormat = "filesize"
percentFormat = "percent"
)
// MetricRow is a tuple of data used to render a metric as a sparkline and
// accoutrements.
type MetricRow struct {
ID string
Label string
Format string
Group string
Value float64
Metric *report.Metric
}
// Copy returns a value copy of the MetricRow
func (m MetricRow) Copy() MetricRow {
metric := m.Metric.Copy()
return MetricRow{
ID: m.ID,
Label: m.Label,
Format: m.Format,
Group: m.Group,
Value: m.Value,
Metric: &metric,
}
}
// MarshalJSON marshals this MetricRow to json. It takes the basic Metric
// rendering, then adds some row-specific fields.
func (m MetricRow) MarshalJSON() ([]byte, error) {
return json.Marshal(struct {
ID string `json:"id"`
Label string `json:"label"`
Format string `json:"format,omitempty"`
Group string `json:"group,omitempty"`
Value float64 `json:"value"`
report.WireMetrics
}{
ID: m.ID,
Label: m.Label,
Format: m.Format,
Group: m.Group,
Value: m.Value,
WireMetrics: m.Metric.ToIntermediate(),
})
}
func metricRow(id, label string, metric report.Metric, format, group string) MetricRow {
var last float64
if s := metric.LastSample(); s != nil {
last = s.Value
}
return MetricRow{
ID: id,
Label: label,
Format: format,
Group: group,
Value: toFixed(last, 2),
Metric: &metric,
}
}
// toFixed truncates decimals of float64 down to specified precision
func toFixed(num float64, precision int) float64 {
output := math.Pow(10, float64(precision))
return float64(int64(num*output)) / output
}
// NodeMetrics 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 NodeMetrics(n report.Node) []MetricRow {
renderers := map[string]func(report.Node) []MetricRow{
"process": processNodeMetrics,
"container": containerNodeMetrics,
"host": hostNodeMetrics,
}
if renderer, ok := renderers[n.Topology]; ok {
return renderer(n)
}
return nil
}
func processNodeMetrics(nmd report.Node) []MetricRow {
rows := []MetricRow{}
for _, tuple := range []struct {
ID, Label, fmt string
}{
{process.CPUUsage, "CPU Usage", percentFormat},
{process.MemoryUsage, "Memory Usage", filesizeFormat},
} {
if val, ok := nmd.Metrics[tuple.ID]; ok {
rows = append(rows, metricRow(
tuple.ID,
tuple.Label,
val,
tuple.fmt,
"",
))
}
}
return rows
}
func containerNodeMetrics(nmd report.Node) []MetricRow {
rows := []MetricRow{}
if val, ok := nmd.Metrics[docker.CPUTotalUsage]; ok {
rows = append(rows, metricRow(
docker.CPUTotalUsage,
"CPU Usage",
val,
percentFormat,
"",
))
}
if val, ok := nmd.Metrics[docker.MemoryUsage]; ok {
rows = append(rows, metricRow(
docker.MemoryUsage,
"Memory Usage",
val,
filesizeFormat,
"",
))
}
return rows
}
func hostNodeMetrics(nmd report.Node) []MetricRow {
// Ensure that all metrics have the same max
maxLoad := 0.0
for _, id := range []string{host.Load1, host.Load5, host.Load15} {
if metric, ok := nmd.Metrics[id]; ok {
if metric.Len() == 0 {
continue
}
if metric.Max > maxLoad {
maxLoad = metric.Max
}
}
}
rows := []MetricRow{}
for _, tuple := range []struct{ ID, Label, fmt string }{
{host.CPUUsage, "CPU Usage", percentFormat},
{host.MemUsage, "Memory Usage", filesizeFormat},
} {
if val, ok := nmd.Metrics[tuple.ID]; ok {
rows = append(rows, metricRow(tuple.ID, tuple.Label, val, tuple.fmt, ""))
}
}
for _, tuple := range []struct{ ID, Label string }{
{host.Load1, "Load (1m)"},
{host.Load5, "Load (5m)"},
{host.Load15, "Load (15m)"},
} {
if val, ok := nmd.Metrics[tuple.ID]; ok {
val.Max = maxLoad
rows = append(rows, metricRow(tuple.ID, tuple.Label, val, defaultFormat, "load"))
}
}
return rows
}
+121
View File
@@ -0,0 +1,121 @@
package detailed_test
import (
"reflect"
"testing"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/render/detailed"
"github.com/weaveworks/scope/report"
"github.com/weaveworks/scope/test"
"github.com/weaveworks/scope/test/fixture"
)
func TestNodeMetrics(t *testing.T) {
inputs := []struct {
name string
node report.Node
want []detailed.MetricRow
}{
{
name: "process",
node: fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
want: []detailed.MetricRow{
{
ID: process.CPUUsage,
Label: "CPU Usage",
Format: "percent",
Group: "",
Value: 0.01,
Metric: &fixture.CPUMetric,
},
{
ID: process.MemoryUsage,
Label: "Memory Usage",
Format: "filesize",
Group: "",
Value: 0.01,
Metric: &fixture.MemoryMetric,
},
},
},
{
name: "container",
node: fixture.Report.Container.Nodes[fixture.ClientContainerNodeID],
want: []detailed.MetricRow{
{
ID: docker.CPUTotalUsage,
Label: "CPU Usage",
Format: "percent",
Group: "",
Value: 0.01,
Metric: &fixture.CPUMetric,
},
{
ID: docker.MemoryUsage,
Label: "Memory Usage",
Format: "filesize",
Group: "",
Value: 0.01,
Metric: &fixture.MemoryMetric,
},
},
},
{
name: "host",
node: fixture.Report.Host.Nodes[fixture.ClientHostNodeID],
want: []detailed.MetricRow{
{
ID: host.CPUUsage,
Label: "CPU Usage",
Format: "percent",
Group: "",
Value: 0.01,
Metric: &fixture.CPUMetric,
},
{
ID: host.MemUsage,
Label: "Memory Usage",
Format: "filesize",
Group: "",
Value: 0.01,
Metric: &fixture.MemoryMetric,
},
{
ID: host.Load1,
Label: "Load (1m)",
Group: "load",
Value: 0.01,
Metric: &fixture.LoadMetric,
},
{
ID: host.Load5,
Label: "Load (5m)",
Group: "load",
Value: 0.01,
Metric: &fixture.LoadMetric,
},
{
ID: host.Load15,
Label: "Load (15m)",
Group: "load",
Value: 0.01,
Metric: &fixture.LoadMetric,
},
},
},
{
name: "unknown topology",
node: report.MakeNode().WithTopology("foobar").WithID(fixture.ClientContainerNodeID),
want: nil,
},
}
for _, input := range inputs {
have := detailed.NodeMetrics(input.node)
if !reflect.DeepEqual(input.want, have) {
t.Errorf("%s: %s", input.name, test.Diff(input.want, have))
}
}
}
+208
View File
@@ -0,0 +1,208 @@
package detailed
import (
"sort"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/report"
)
// Node is the data type that's yielded to the JavaScript layer when
// we want deep information about an individual node.
type Node struct {
ID string `json:"id"`
Label string `json:"label"`
Rank string `json:"rank,omitempty"`
Pseudo bool `json:"pseudo,omitempty"`
Controls []ControlInstance `json:"controls"`
Metadata []MetadataRow `json:"metadata,omitempty"`
Metrics []MetricRow `json:"metrics,omitempty"`
Children []NodeSummaryGroup `json:"children,omitempty"`
Parents []Parent `json:"parents,omitempty"`
}
// Parent is the information needed to build a link to the parent of a Node.
type Parent struct {
ID string `json:"id"`
Label string `json:"label"`
TopologyID string `json:"topologyId"`
}
// ControlInstance contains a control description, and all the info
// needed to execute it.
type ControlInstance struct {
ProbeID string `json:"probeId"`
NodeID string `json:"nodeId"`
report.Control
}
// MakeNode transforms a renderable node to a detailed node. It uses
// aggregate metadata, plus the set of origin node IDs, to produce tables.
func MakeNode(r report.Report, n render.RenderableNode) Node {
return Node{
ID: n.ID,
Label: n.LabelMajor,
Rank: n.Rank,
Pseudo: n.Pseudo,
Controls: controls(r, n),
Metadata: NodeMetadata(n.Node),
Metrics: NodeMetrics(n.Node),
Children: children(n),
Parents: parents(r, n),
}
}
func controlsFor(topology report.Topology, nodeID string) []ControlInstance {
result := []ControlInstance{}
node, ok := topology.Nodes[nodeID]
if !ok {
return result
}
for _, id := range node.Controls.Controls {
if control, ok := topology.Controls[id]; ok {
result = append(result, ControlInstance{
ProbeID: node.Metadata[report.ProbeID],
NodeID: nodeID,
Control: control,
})
}
}
return result
}
func controls(r report.Report, n render.RenderableNode) []ControlInstance {
if _, ok := r.Process.Nodes[n.ControlNode]; ok {
return controlsFor(r.Process, n.ControlNode)
} else if _, ok := r.Container.Nodes[n.ControlNode]; ok {
return controlsFor(r.Container, n.ControlNode)
} else if _, ok := r.ContainerImage.Nodes[n.ControlNode]; ok {
return controlsFor(r.ContainerImage, n.ControlNode)
} else if _, ok := r.Host.Nodes[n.ControlNode]; ok {
return controlsFor(r.Host, n.ControlNode)
}
return []ControlInstance{}
}
var (
nodeSummaryGroupSpecs = []struct {
topologyID string
NodeSummaryGroup
}{
{"host", NodeSummaryGroup{TopologyID: "hosts", Label: "Hosts", Columns: []string{host.CPUUsage, host.MemUsage}}},
{"pod", NodeSummaryGroup{TopologyID: "pods", Label: "Pods", Columns: []string{}}},
{"container_image", NodeSummaryGroup{TopologyID: "containers-by-image", Label: "Container Images", Columns: []string{}}},
{"container", NodeSummaryGroup{TopologyID: "containers", Label: "Containers", Columns: []string{docker.CPUTotalUsage, docker.MemoryUsage}}},
{"process", NodeSummaryGroup{TopologyID: "applications", Label: "Applications", Columns: []string{process.PID, process.CPUUsage, process.MemoryUsage}}},
}
)
func children(n render.RenderableNode) []NodeSummaryGroup {
summaries := map[string][]NodeSummary{}
for _, child := range n.Children {
if child.ID == n.ID {
continue
}
if summary, ok := MakeNodeSummary(child); ok {
summaries[child.Topology] = append(summaries[child.Topology], summary)
}
}
nodeSummaryGroups := []NodeSummaryGroup{}
for _, spec := range nodeSummaryGroupSpecs {
if len(summaries[spec.topologyID]) > 0 {
sort.Sort(nodeSummariesByID(summaries[spec.TopologyID]))
group := spec.NodeSummaryGroup.Copy()
group.Nodes = summaries[spec.topologyID]
nodeSummaryGroups = append(nodeSummaryGroups, group)
}
}
return nodeSummaryGroups
}
// parents is a total a hack to find the parents of a node (which is
// ill-defined).
func parents(r report.Report, n render.RenderableNode) (result []Parent) {
defer func() {
for i, parent := range result {
if parent.ID == n.ID {
result = append(result[:i], result[i+1:]...)
}
}
}()
topologies := map[string]struct {
report.Topology
render func(report.Node) Parent
}{
"container": {r.Container, containerParent},
"pod": {r.Pod, podParent},
"service": {r.Service, serviceParent},
"container_image": {r.ContainerImage, containerImageParent},
"host": {r.Host, hostParent},
}
topologyIDs := []string{}
for topologyID := range topologies {
topologyIDs = append(topologyIDs, topologyID)
}
sort.Strings(topologyIDs)
for _, topologyID := range topologyIDs {
t := topologies[topologyID]
for _, id := range n.Node.Parents[topologyID] {
parent, ok := t.Nodes[id]
if !ok {
continue
}
result = append(result, t.render(parent))
}
}
return result
}
func containerParent(n report.Node) Parent {
label, _ := render.GetRenderableContainerName(n)
return Parent{
ID: render.MakeContainerID(n.Metadata[docker.ContainerID]),
Label: label,
TopologyID: "containers",
}
}
func podParent(n report.Node) Parent {
return Parent{
ID: render.MakePodID(n.Metadata[kubernetes.PodID]),
Label: n.Metadata[kubernetes.PodName],
TopologyID: "pods",
}
}
func serviceParent(n report.Node) Parent {
return Parent{
ID: render.MakeServiceID(n.Metadata[kubernetes.ServiceID]),
Label: n.Metadata[kubernetes.ServiceName],
TopologyID: "pods-by-service",
}
}
func containerImageParent(n report.Node) Parent {
imageName := n.Metadata[docker.ImageName]
return Parent{
ID: render.MakeContainerImageID(render.ImageNameWithoutVersion(imageName)),
Label: imageName,
TopologyID: "containers-by-image",
}
}
func hostParent(n report.Node) Parent {
return Parent{
ID: render.MakeHostID(n.Metadata[host.HostName]),
Label: n.Metadata[host.HostName],
TopologyID: "hosts",
}
}
+185
View File
@@ -0,0 +1,185 @@
package detailed_test
import (
"fmt"
"reflect"
"testing"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/render/detailed"
"github.com/weaveworks/scope/test"
"github.com/weaveworks/scope/test/fixture"
)
func TestMakeDetailedHostNode(t *testing.T) {
renderableNode := render.HostRenderer.Render(fixture.Report)[render.MakeHostID(fixture.ClientHostID)]
have := detailed.MakeNode(fixture.Report, renderableNode)
containerImageNodeSummary, _ := detailed.MakeNodeSummary(fixture.Report.ContainerImage.Nodes[fixture.ClientContainerImageNodeID])
containerNodeSummary, _ := detailed.MakeNodeSummary(fixture.Report.Container.Nodes[fixture.ClientContainerNodeID])
process1NodeSummary, _ := detailed.MakeNodeSummary(fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID])
process1NodeSummary.Linkable = true
process2NodeSummary, _ := detailed.MakeNodeSummary(fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID])
process2NodeSummary.Linkable = true
want := detailed.Node{
ID: render.MakeHostID(fixture.ClientHostID),
Label: "client",
Rank: "hostname.com",
Pseudo: false,
Controls: []detailed.ControlInstance{},
Metadata: []detailed.MetadataRow{
{
ID: "host_name",
Label: "Hostname",
Value: "client.hostname.com",
},
{
ID: "os",
Label: "Operating system",
Value: "Linux",
},
{
ID: "local_networks",
Label: "Local Networks",
Value: "10.10.10.0/24",
},
},
Metrics: []detailed.MetricRow{
{
ID: host.CPUUsage,
Format: "percent",
Label: "CPU Usage",
Value: 0.01,
Metric: &fixture.CPUMetric,
},
{
ID: host.MemUsage,
Format: "filesize",
Label: "Memory Usage",
Value: 0.01,
Metric: &fixture.MemoryMetric,
},
{
ID: host.Load1,
Group: "load",
Label: "Load (1m)",
Value: 0.01,
Metric: &fixture.LoadMetric,
},
{
ID: host.Load5,
Group: "load",
Label: "Load (5m)",
Value: 0.01,
Metric: &fixture.LoadMetric,
},
{
ID: host.Load15,
Label: "Load (15m)",
Group: "load",
Value: 0.01,
Metric: &fixture.LoadMetric,
},
},
Children: []detailed.NodeSummaryGroup{
{
Label: "Container Images",
TopologyID: "containers-by-image",
Columns: []string{},
Nodes: []detailed.NodeSummary{containerImageNodeSummary},
},
{
Label: "Containers",
TopologyID: "containers",
Columns: []string{docker.CPUTotalUsage, docker.MemoryUsage},
Nodes: []detailed.NodeSummary{containerNodeSummary},
},
{
Label: "Applications",
TopologyID: "applications",
Columns: []string{process.PID, process.CPUUsage, process.MemoryUsage},
Nodes: []detailed.NodeSummary{process1NodeSummary, process2NodeSummary},
},
},
}
if !reflect.DeepEqual(want, have) {
t.Errorf("%s", test.Diff(want, have))
}
}
func TestMakeDetailedContainerNode(t *testing.T) {
id := render.MakeContainerID(fixture.ServerContainerID)
renderableNode, ok := render.ContainerRenderer.Render(fixture.Report)[id]
if !ok {
t.Fatalf("Node not found: %s", id)
}
have := detailed.MakeNode(fixture.Report, renderableNode)
want := detailed.Node{
ID: id,
Label: "server",
Rank: "imageid456",
Pseudo: false,
Controls: []detailed.ControlInstance{},
Metadata: []detailed.MetadataRow{
{ID: "docker_container_id", Label: "ID", Value: fixture.ServerContainerID},
{ID: "docker_image_id", Label: "Image ID", Value: fixture.ServerContainerImageID},
{ID: "docker_container_state", Label: "State", Value: "running"},
{ID: "label_" + render.AmazonECSContainerNameLabel, Label: fmt.Sprintf(`Label %q`, render.AmazonECSContainerNameLabel), Value: `server`},
{ID: "label_foo1", Label: `Label "foo1"`, Value: `bar1`},
{ID: "label_foo2", Label: `Label "foo2"`, Value: `bar2`},
{ID: "label_io.kubernetes.pod.name", Label: `Label "io.kubernetes.pod.name"`, Value: "ping/pong-b"},
},
Metrics: []detailed.MetricRow{
{
ID: docker.CPUTotalUsage,
Format: "percent",
Label: "CPU Usage",
Value: 0.01,
Metric: &fixture.CPUMetric,
},
{
ID: docker.MemoryUsage,
Format: "filesize",
Label: "Memory Usage",
Value: 0.01,
Metric: &fixture.MemoryMetric,
},
},
Children: []detailed.NodeSummaryGroup{
{
Label: "Applications",
TopologyID: "applications",
Columns: []string{process.PID, process.CPUUsage, process.MemoryUsage},
Nodes: []detailed.NodeSummary{
{
ID: fmt.Sprintf("process:%s:%s", "server.hostname.com", fixture.ServerPID),
Label: "apache",
Linkable: true,
Metadata: []detailed.MetadataRow{
{ID: process.PID, Label: "PID", Value: fixture.ServerPID},
},
Metrics: []detailed.MetricRow{},
},
},
},
},
Parents: []detailed.Parent{
{
ID: render.MakeContainerImageID(fixture.ServerContainerImageName),
Label: fixture.ServerContainerImageName,
TopologyID: "containers-by-image",
},
{
ID: render.MakeHostID(fixture.ServerHostName),
Label: fixture.ServerHostName,
TopologyID: "hosts",
},
},
}
if !reflect.DeepEqual(want, have) {
t.Errorf("%s", test.Diff(want, have))
}
}
+140
View File
@@ -0,0 +1,140 @@
package detailed
import (
"fmt"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/probe/kubernetes"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/report"
)
// NodeSummaryGroup is a topology-typed group of children for a Node.
type NodeSummaryGroup struct {
Label string `json:"label"`
Nodes []NodeSummary `json:"nodes"`
TopologyID string `json:"topologyId"`
Columns []string `json:"columns"`
}
// Copy returns a value copy of the NodeSummaryGroup
func (g NodeSummaryGroup) Copy() NodeSummaryGroup {
result := NodeSummaryGroup{
TopologyID: g.TopologyID,
Label: g.Label,
Columns: g.Columns,
}
for _, node := range g.Nodes {
result.Nodes = append(result.Nodes, node.Copy())
}
return result
}
// NodeSummary is summary information about a child for a Node.
type NodeSummary struct {
ID string `json:"id"`
Label string `json:"label"`
Linkable bool `json:"linkable"` // Whether this node can be linked-to
Metadata []MetadataRow `json:"metadata,omitempty"`
Metrics []MetricRow `json:"metrics,omitempty"`
}
// MakeNodeSummary summarizes a node, if possible.
func MakeNodeSummary(n report.Node) (NodeSummary, bool) {
renderers := map[string]func(report.Node) NodeSummary{
"process": processNodeSummary,
"container": containerNodeSummary,
"container_image": containerImageNodeSummary,
"pod": podNodeSummary,
"host": hostNodeSummary,
}
if renderer, ok := renderers[n.Topology]; ok {
return renderer(n), true
}
return NodeSummary{}, false
}
// Copy returns a value copy of the NodeSummary
func (n NodeSummary) Copy() NodeSummary {
result := NodeSummary{
ID: n.ID,
Label: n.Label,
Linkable: n.Linkable,
}
for _, row := range n.Metadata {
result.Metadata = append(result.Metadata, row.Copy())
}
for _, row := range n.Metrics {
result.Metrics = append(result.Metrics, row.Copy())
}
return result
}
func processNodeSummary(nmd report.Node) NodeSummary {
var (
id string
label, nameFound = nmd.Metadata[process.Name]
)
if pid, ok := nmd.Metadata[process.PID]; ok {
if !nameFound {
label = fmt.Sprintf("(%s)", pid)
}
id = render.MakeProcessID(report.ExtractHostID(nmd), pid)
}
_, isConnected := nmd.Metadata[render.IsConnected]
return NodeSummary{
ID: id,
Label: label,
Linkable: isConnected,
Metadata: processNodeMetadata(nmd),
Metrics: processNodeMetrics(nmd),
}
}
func containerNodeSummary(nmd report.Node) NodeSummary {
label, _ := render.GetRenderableContainerName(nmd)
return NodeSummary{
ID: render.MakeContainerID(nmd.Metadata[docker.ContainerID]),
Label: label,
Linkable: true,
Metadata: containerNodeMetadata(nmd),
Metrics: containerNodeMetrics(nmd),
}
}
func containerImageNodeSummary(nmd report.Node) NodeSummary {
imageName := nmd.Metadata[docker.ImageName]
return NodeSummary{
ID: render.MakeContainerImageID(render.ImageNameWithoutVersion(imageName)),
Label: imageName,
Linkable: true,
Metadata: containerImageNodeMetadata(nmd),
}
}
func podNodeSummary(nmd report.Node) NodeSummary {
return NodeSummary{
ID: render.MakePodID(nmd.Metadata[kubernetes.PodID]),
Label: nmd.Metadata[kubernetes.PodName],
Linkable: true,
Metadata: podNodeMetadata(nmd),
}
}
func hostNodeSummary(nmd report.Node) NodeSummary {
return NodeSummary{
ID: render.MakeHostID(nmd.Metadata[host.HostName]),
Label: nmd.Metadata[host.HostName],
Linkable: true,
Metadata: hostNodeMetadata(nmd),
Metrics: hostNodeMetrics(nmd),
}
}
type nodeSummariesByID []NodeSummary
func (s nodeSummariesByID) Len() int { return len(s) }
func (s nodeSummariesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s nodeSummariesByID) Less(i, j int) bool { return s[i].ID < s[j].ID }
-572
View File
@@ -1,572 +0,0 @@
package render
import (
"fmt"
"sort"
"strconv"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/host"
"github.com/weaveworks/scope/probe/overlay"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/report"
)
const (
containerImageRank = 4
containerRank = 3
processRank = 2
hostRank = 1
connectionsRank = 0 // keep connections at the bottom until they are expandable in the UI
)
// DetailedNode is the data type that's yielded to the JavaScript layer when
// we want deep information about an individual node.
type DetailedNode struct {
ID string `json:"id"`
LabelMajor string `json:"label_major"`
LabelMinor string `json:"label_minor,omitempty"`
Rank string `json:"rank,omitempty"`
Pseudo bool `json:"pseudo,omitempty"`
Tables []Table `json:"tables"`
Controls []ControlInstance `json:"controls"`
}
// Table is a dataset associated with a node. It will be displayed in the
// detail panel when a user clicks on a node.
type Table struct {
Title string `json:"title"` // e.g. Bandwidth
Numeric bool `json:"numeric"` // should the major column be right-aligned?
Rank int `json:"-"` // used to sort tables; not emitted.
Rows []Row `json:"rows"`
}
// Row is a single entry in a Table dataset.
type Row struct {
Key string `json:"key"` // e.g. Ingress
ValueMajor string `json:"value_major"` // e.g. 25
ValueMinor string `json:"value_minor,omitempty"` // e.g. KB/s
Expandable bool `json:"expandable,omitempty"` // Whether it can be expanded (hidden by default)
ValueType string `json:"value_type,omitempty"` // e.g. sparkline
Metric *report.Metric `json:"metric,omitempty"` // e.g. sparkline data samples
}
// ControlInstance contains a control description, and all the info
// needed to execute it.
type ControlInstance struct {
ProbeID string `json:"probeId"`
NodeID string `json:"nodeId"`
report.Control
}
type sortableRows []Row
func (r sortableRows) Len() int { return len(r) }
func (r sortableRows) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r sortableRows) Less(i, j int) bool {
switch {
case r[i].Key != r[j].Key:
return r[i].Key < r[j].Key
case r[i].ValueMajor != r[j].ValueMajor:
return r[i].ValueMajor < r[j].ValueMajor
default:
return r[i].ValueMinor < r[j].ValueMinor
}
}
type sortableTables []Table
func (t sortableTables) Len() int { return len(t) }
func (t sortableTables) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t sortableTables) 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 {
tables := sortableTables{}
// Figure out if multiple hosts/containers are referenced by the renderableNode
multiContainer, multiHost := getRenderingContext(r, n)
// 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
// in the UI, so we skip the intermediate representations, but we could
// add them later.
connections := []Row{}
for _, id := range n.Origins {
if table, ok := OriginTable(r, id, multiHost, multiContainer); ok {
tables = append(tables, table)
} else if _, ok := r.Endpoint.Nodes[id]; ok {
connections = append(connections, connectionDetailsRows(r.Endpoint, id)...)
} else if _, ok := r.Address.Nodes[id]; ok {
connections = append(connections, connectionDetailsRows(r.Address, id)...)
}
}
if table, ok := connectionsTable(connections, r, n); ok {
tables = append(tables, table)
}
// Sort tables by rank
sort.Sort(tables)
return DetailedNode{
ID: n.ID,
LabelMajor: n.LabelMajor,
LabelMinor: n.LabelMinor,
Rank: n.Rank,
Pseudo: n.Pseudo,
Tables: tables,
Controls: controls(r, n),
}
}
func getRenderingContext(r report.Report, n RenderableNode) (multiContainer, multiHost bool) {
var (
originHosts = map[string]struct{}{}
originContainers = map[string]struct{}{}
)
for _, id := range n.Origins {
for _, topology := range r.Topologies() {
if nmd, ok := topology.Nodes[id]; ok {
originHosts[report.ExtractHostID(nmd)] = struct{}{}
if id, ok := nmd.Metadata[docker.ContainerID]; ok {
originContainers[id] = struct{}{}
}
}
// Return early if possible
multiHost = len(originHosts) > 1
multiContainer = len(originContainers) > 1
if multiHost && multiContainer {
return
}
}
}
return
}
func connectionsTable(connections []Row, r report.Report, n RenderableNode) (Table, bool) {
sec := r.Window.Seconds()
rate := func(u *uint64) (float64, bool) {
if u == nil {
return 0.0, false
}
if sec <= 0 {
return 0.0, true
}
return float64(*u) / sec, true
}
shortenByteRate := func(rate float64) (major, minor string) {
switch {
case rate > 1024*1024:
return fmt.Sprintf("%.2f", rate/1024/1024), "MBps"
case rate > 1024:
return fmt.Sprintf("%.1f", rate/1024), "KBps"
default:
return fmt.Sprintf("%.0f", rate), "Bps"
}
}
rows := []Row{}
if n.EdgeMetadata.MaxConnCountTCP != nil {
rows = append(rows, Row{Key: "TCP connections", ValueMajor: strconv.FormatUint(*n.EdgeMetadata.MaxConnCountTCP, 10)})
}
if rate, ok := rate(n.EdgeMetadata.EgressPacketCount); ok {
rows = append(rows, Row{Key: "Egress packet rate", ValueMajor: fmt.Sprintf("%.0f", rate), ValueMinor: "packets/sec"})
}
if rate, ok := rate(n.EdgeMetadata.IngressPacketCount); ok {
rows = append(rows, Row{Key: "Ingress packet rate", ValueMajor: fmt.Sprintf("%.0f", rate), ValueMinor: "packets/sec"})
}
if rate, ok := rate(n.EdgeMetadata.EgressByteCount); ok {
s, unit := shortenByteRate(rate)
rows = append(rows, Row{Key: "Egress byte rate", ValueMajor: s, ValueMinor: unit})
}
if rate, ok := rate(n.EdgeMetadata.IngressByteCount); ok {
s, unit := shortenByteRate(rate)
rows = append(rows, Row{Key: "Ingress byte rate", ValueMajor: s, ValueMinor: unit})
}
if len(connections) > 0 {
sort.Sort(sortableRows(connections))
rows = append(rows, Row{Key: "Client", ValueMajor: "Server", Expandable: true})
rows = append(rows, connections...)
}
if len(rows) > 0 {
return Table{
Title: "Connections",
Numeric: false,
Rank: connectionsRank,
Rows: rows,
}, true
}
return Table{}, false
}
func controlsFor(topology report.Topology, nodeID string) []ControlInstance {
result := []ControlInstance{}
node, ok := topology.Nodes[nodeID]
if !ok {
return result
}
for _, id := range node.Controls.Controls {
if control, ok := topology.Controls[id]; ok {
result = append(result, ControlInstance{
ProbeID: node.Metadata[report.ProbeID],
NodeID: nodeID,
Control: control,
})
}
}
return result
}
func controls(r report.Report, n RenderableNode) []ControlInstance {
if _, ok := r.Process.Nodes[n.ControlNode]; ok {
return controlsFor(r.Process, n.ControlNode)
} else if _, ok := r.Container.Nodes[n.ControlNode]; ok {
return controlsFor(r.Container, n.ControlNode)
} else if _, ok := r.ContainerImage.Nodes[n.ControlNode]; ok {
return controlsFor(r.ContainerImage, n.ControlNode)
} else if _, ok := r.Host.Nodes[n.ControlNode]; ok {
return controlsFor(r.Host, n.ControlNode)
}
return []ControlInstance{}
}
// 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, addHostTags bool, addContainerTags bool) (Table, bool) {
result, show := Table{}, false
if nmd, ok := r.Process.Nodes[originID]; ok {
result, show = processOriginTable(nmd, addHostTags, addContainerTags)
}
if nmd, ok := r.Container.Nodes[originID]; ok {
result, show = containerOriginTable(nmd, addHostTags)
}
if nmd, ok := r.ContainerImage.Nodes[originID]; ok {
result, show = containerImageOriginTable(nmd)
}
if nmd, ok := r.Host.Nodes[originID]; ok {
result, show = hostOriginTable(nmd)
}
return result, show
}
func connectionDetailsRows(topology report.Topology, originID string) []Row {
rows := []Row{}
labeler := func(nodeID string, sets report.Sets) (string, bool) {
if _, addr, port, ok := report.ParseEndpointNodeID(nodeID); ok {
if names, ok := sets["name"]; ok {
return fmt.Sprintf("%s:%s", names[0], port), true
}
return fmt.Sprintf("%s:%s", addr, port), true
}
if _, addr, ok := report.ParseAddressNodeID(nodeID); ok {
return addr, true
}
return "", false
}
local, ok := labeler(originID, topology.Nodes[originID].Sets)
if !ok {
return rows
}
// Firstly, collection outgoing connections from this node.
for _, serverNodeID := range topology.Nodes[originID].Adjacency {
remote, ok := labeler(serverNodeID, topology.Nodes[serverNodeID].Sets)
if !ok {
continue
}
rows = append(rows, Row{
Key: local,
ValueMajor: remote,
Expandable: true,
})
}
// Next, scan the topology for incoming connections to this node.
for clientNodeID, clientNode := range topology.Nodes {
if clientNodeID == originID {
continue
}
serverNodeIDs := clientNode.Adjacency
if !serverNodeIDs.Contains(originID) {
continue
}
remote, ok := labeler(clientNodeID, clientNode.Sets)
if !ok {
continue
}
rows = append(rows, Row{
Key: remote,
ValueMajor: local,
ValueMinor: "",
Expandable: true,
})
}
return rows
}
func processOriginTable(nmd report.Node, addHostTag bool, addContainerTag bool) (Table, bool) {
rows := []Row{}
for _, tuple := range []struct{ key, human string }{
{process.PPID, "Parent PID"},
{process.Cmdline, "Command"},
{process.Threads, "# Threads"},
} {
if val, ok := nmd.Metadata[tuple.key]; ok {
rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
}
}
if containerID, ok := nmd.Metadata[docker.ContainerID]; ok && addContainerTag {
rows = append([]Row{{Key: "Container ID", ValueMajor: containerID}}, rows...)
}
if addHostTag {
rows = append([]Row{{Key: "Host", ValueMajor: report.ExtractHostID(nmd)}}, rows...)
}
for _, tuple := range []struct {
key, human string
fmt formatter
}{
{process.CPUUsage, "CPU Usage", formatPercent},
{process.MemoryUsage, "Memory Usage", formatMemory},
} {
if val, ok := nmd.Metrics[tuple.key]; ok {
rows = append(rows, sparklineRow(tuple.human, val, tuple.fmt))
}
}
var (
title = "Process"
name, commFound = nmd.Metadata[process.Name]
pid, pidFound = nmd.Metadata[process.PID]
)
if commFound {
title += ` "` + name + `"`
}
if pidFound {
title += " (" + pid + ")"
}
return Table{
Title: title,
Numeric: false,
Rows: rows,
Rank: processRank,
}, len(rows) > 0 || commFound || pidFound
}
type formatter func(report.Metric) (report.Metric, string)
func sparklineRow(human string, metric report.Metric, format formatter) Row {
if format == nil {
format = formatDefault
}
metric, lastStr := format(metric)
return Row{Key: human, ValueMajor: lastStr, Metric: &metric, ValueType: "sparkline"}
}
func formatDefault(m report.Metric) (report.Metric, string) {
if s := m.LastSample(); s != nil {
return m, fmt.Sprintf("%0.2f", s.Value)
}
return m, ""
}
func memoryScale(n float64) (string, float64) {
brackets := []struct {
human string
shift uint
}{
{"bytes", 0},
{"KB", 10},
{"MB", 20},
{"GB", 30},
{"TB", 40},
{"PB", 50},
}
for _, bracket := range brackets {
unit := (1 << bracket.shift)
if n < float64(unit<<10) {
return bracket.human, float64(unit)
}
}
return "PB", float64(1 << 50)
}
func formatMemory(m report.Metric) (report.Metric, string) {
s := m.LastSample()
if s == nil {
return m, ""
}
human, divisor := memoryScale(s.Value)
return m.Div(divisor), fmt.Sprintf("%0.2f %s", s.Value/divisor, human)
}
func formatPercent(m report.Metric) (report.Metric, string) {
if s := m.LastSample(); s != nil {
return m, fmt.Sprintf("%0.2f%%", s.Value)
}
return m, ""
}
func containerOriginTable(nmd report.Node, addHostTag bool) (Table, bool) {
rows := []Row{}
for _, tuple := range []struct{ key, human string }{
{docker.ContainerState, "State"},
} {
if val, ok := nmd.Latest.Lookup(tuple.key); ok && val != "" {
rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
}
}
for _, tuple := range []struct{ key, human string }{
{docker.ContainerID, "ID"},
{docker.ImageID, "Image ID"},
{docker.ContainerPorts, "Ports"},
{docker.ContainerCreated, "Created"},
{docker.ContainerCommand, "Command"},
{overlay.WeaveMACAddress, "Weave MAC"},
{overlay.WeaveDNSHostname, "Weave DNS Hostname"},
} {
if val, ok := nmd.Metadata[tuple.key]; ok && val != "" {
rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
}
}
for _, ip := range docker.ExtractContainerIPs(nmd) {
rows = append(rows, Row{Key: "IP Address", ValueMajor: ip, ValueMinor: ""})
}
rows = append(rows, getDockerLabelRows(nmd)...)
if addHostTag {
rows = append([]Row{{Key: "Host", ValueMajor: report.ExtractHostID(nmd)}}, rows...)
}
if val, ok := nmd.Metrics[docker.MemoryUsage]; ok {
rows = append(rows, sparklineRow("Memory Usage", val, formatMemory))
}
if val, ok := nmd.Metrics[docker.CPUTotalUsage]; ok {
rows = append(rows, sparklineRow("CPU Usage", val, formatPercent))
}
var (
title = "Container"
name, nameFound = GetRenderableContainerName(nmd)
)
if nameFound {
title += ` "` + name + `"`
}
return Table{
Title: title,
Numeric: false,
Rows: rows,
Rank: containerRank,
}, len(rows) > 0 || nameFound
}
func containerImageOriginTable(nmd report.Node) (Table, bool) {
rows := []Row{}
for _, tuple := range []struct{ key, human string }{
{docker.ImageID, "Image ID"},
} {
if val, ok := nmd.Metadata[tuple.key]; ok {
rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
}
}
rows = append(rows, getDockerLabelRows(nmd)...)
title := "Container Image"
var (
nameFound bool
name string
)
if name, nameFound = nmd.Metadata[docker.ImageName]; nameFound {
title += ` "` + name + `"`
}
return Table{
Title: title,
Numeric: false,
Rows: rows,
Rank: containerImageRank,
}, len(rows) > 0 || nameFound
}
func getDockerLabelRows(nmd report.Node) []Row {
rows := []Row{}
// Add labels in alphabetical order
labels := docker.ExtractLabels(nmd)
labelKeys := make([]string, 0, len(labels))
for k := range labels {
labelKeys = append(labelKeys, k)
}
sort.Strings(labelKeys)
for _, labelKey := range labelKeys {
rows = append(rows, Row{Key: fmt.Sprintf("Label %q", labelKey), ValueMajor: labels[labelKey]})
}
return rows
}
func hostOriginTable(nmd report.Node) (Table, bool) {
// Ensure that all metrics have the same max
maxLoad := 0.0
for _, key := range []string{host.Load1, host.Load5, host.Load15} {
if metric, ok := nmd.Metrics[key]; ok {
if metric.Len() == 0 {
continue
}
if metric.Max > maxLoad {
maxLoad = metric.Max
}
}
}
rows := []Row{}
for _, tuple := range []struct{ key, human string }{
{host.Load1, "Load (1m)"},
{host.Load5, "Load (5m)"},
{host.Load15, "Load (15m)"},
} {
if val, ok := nmd.Metrics[tuple.key]; ok {
val.Max = maxLoad
rows = append(rows, sparklineRow(tuple.human, val, nil))
}
}
for _, tuple := range []struct {
key, human string
fmt formatter
}{
{host.CPUUsage, "CPU Usage", formatPercent},
{host.MemUsage, "Memory Usage", formatMemory},
} {
if val, ok := nmd.Metrics[tuple.key]; ok {
rows = append(rows, sparklineRow(tuple.human, val, tuple.fmt))
}
}
for _, tuple := range []struct{ key, human string }{
{host.OS, "Operating system"},
{host.KernelVersion, "Kernel version"},
{host.Uptime, "Uptime"},
} {
if val, ok := nmd.Metadata[tuple.key]; ok {
rows = append(rows, Row{Key: tuple.human, ValueMajor: val, ValueMinor: ""})
}
}
title := "Host"
var (
name string
foundName bool
)
if name, foundName = nmd.Metadata[host.HostName]; foundName {
title += ` "` + name + `"`
}
return Table{
Title: title,
Numeric: false,
Rows: rows,
Rank: hostRank,
}, len(rows) > 0 || foundName
}
-249
View File
@@ -1,249 +0,0 @@
package render_test
import (
"fmt"
"reflect"
"testing"
"github.com/weaveworks/scope/render"
"github.com/weaveworks/scope/test"
"github.com/weaveworks/scope/test/fixture"
)
func TestOriginTable(t *testing.T) {
if _, ok := render.OriginTable(fixture.Report, "not-found", false, false); ok {
t.Errorf("unknown origin ID gave unexpected success")
}
for originID, want := range map[string]render.Table{
fixture.ServerProcessNodeID: {
Title: fmt.Sprintf(`Process "apache" (%s)`, fixture.ServerPID),
Numeric: false,
Rank: 2,
Rows: []render.Row{},
},
fixture.ServerHostNodeID: {
Title: fmt.Sprintf("Host %q", fixture.ServerHostName),
Numeric: false,
Rank: 1,
Rows: []render.Row{
{Key: "Load (1m)", ValueMajor: "0.01", Metric: &fixture.LoadMetric, ValueType: "sparkline"},
{Key: "Load (5m)", ValueMajor: "0.01", Metric: &fixture.LoadMetric, ValueType: "sparkline"},
{Key: "Load (15m)", ValueMajor: "0.01", Metric: &fixture.LoadMetric, ValueType: "sparkline"},
{Key: "Operating system", ValueMajor: "Linux"},
},
},
} {
have, ok := render.OriginTable(fixture.Report, originID, false, false)
if !ok {
t.Errorf("%q: not OK", originID)
continue
}
if !reflect.DeepEqual(want, have) {
t.Errorf("%q: %s", originID, test.Diff(want, have))
}
}
// Test host/container tags
for originID, want := range map[string]render.Table{
fixture.ServerProcessNodeID: {
Title: fmt.Sprintf(`Process "apache" (%s)`, fixture.ServerPID),
Numeric: false,
Rank: 2,
Rows: []render.Row{
{Key: "Host", ValueMajor: fixture.ServerHostID},
{Key: "Container ID", ValueMajor: fixture.ServerContainerID},
},
},
fixture.ServerContainerNodeID: {
Title: `Container "server"`,
Numeric: false,
Rank: 3,
Rows: []render.Row{
{Key: "Host", ValueMajor: fixture.ServerHostID},
{Key: "State", ValueMajor: "running"},
{Key: "ID", ValueMajor: fixture.ServerContainerID},
{Key: "Image ID", ValueMajor: fixture.ServerContainerImageID},
{Key: fmt.Sprintf(`Label %q`, render.AmazonECSContainerNameLabel), ValueMajor: `server`},
{Key: `Label "foo1"`, ValueMajor: `bar1`},
{Key: `Label "foo2"`, ValueMajor: `bar2`},
{Key: `Label "io.kubernetes.pod.name"`, ValueMajor: "ping/pong-b"},
},
},
} {
have, ok := render.OriginTable(fixture.Report, originID, true, 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(fixture.Report)[render.MakeHostID(fixture.ClientHostID)]
have := render.MakeDetailedNode(fixture.Report, renderableNode)
want := render.DetailedNode{
ID: render.MakeHostID(fixture.ClientHostID),
LabelMajor: "client",
LabelMinor: "hostname.com",
Rank: "hostname.com",
Pseudo: false,
Controls: []render.ControlInstance{},
Tables: []render.Table{
{
Title: fmt.Sprintf("Host %q", fixture.ClientHostName),
Numeric: false,
Rank: 1,
Rows: []render.Row{
{
Key: "Load (1m)",
ValueMajor: "0.01",
Metric: &fixture.LoadMetric,
ValueType: "sparkline",
},
{
Key: "Load (5m)",
ValueMajor: "0.01",
Metric: &fixture.LoadMetric,
ValueType: "sparkline",
},
{
Key: "Load (15m)",
ValueMajor: "0.01",
Metric: &fixture.LoadMetric,
ValueType: "sparkline",
},
{
Key: "Operating system",
ValueMajor: "Linux",
},
},
},
{
Title: "Connections",
Numeric: false,
Rank: 0,
Rows: []render.Row{
{
Key: "TCP connections",
ValueMajor: "3",
},
{
Key: "Client",
ValueMajor: "Server",
Expandable: true,
},
{
Key: "10.10.10.20",
ValueMajor: "192.168.1.1",
Expandable: true,
},
},
},
},
}
if !reflect.DeepEqual(want, have) {
t.Errorf("%s", test.Diff(want, have))
}
}
func TestMakeDetailedContainerNode(t *testing.T) {
renderableNode := render.ContainerRenderer.Render(fixture.Report)[fixture.ServerContainerID]
have := render.MakeDetailedNode(fixture.Report, renderableNode)
want := render.DetailedNode{
ID: fixture.ServerContainerID,
LabelMajor: "server",
LabelMinor: fixture.ServerHostName,
Rank: "imageid456",
Pseudo: false,
Controls: []render.ControlInstance{},
Tables: []render.Table{
{
Title: `Container Image "image/server"`,
Numeric: false,
Rank: 4,
Rows: []render.Row{
{Key: "Image ID", ValueMajor: fixture.ServerContainerImageID},
{Key: `Label "foo1"`, ValueMajor: `bar1`},
{Key: `Label "foo2"`, ValueMajor: `bar2`},
},
},
{
Title: `Container "server"`,
Numeric: false,
Rank: 3,
Rows: []render.Row{
{Key: "State", ValueMajor: "running"},
{Key: "ID", ValueMajor: fixture.ServerContainerID},
{Key: "Image ID", ValueMajor: fixture.ServerContainerImageID},
{Key: fmt.Sprintf(`Label %q`, render.AmazonECSContainerNameLabel), ValueMajor: `server`},
{Key: `Label "foo1"`, ValueMajor: `bar1`},
{Key: `Label "foo2"`, ValueMajor: `bar2`},
{Key: `Label "io.kubernetes.pod.name"`, ValueMajor: "ping/pong-b"},
},
},
{
Title: fmt.Sprintf(`Process "apache" (%s)`, fixture.ServerPID),
Numeric: false,
Rank: 2,
Rows: []render.Row{},
},
{
Title: fmt.Sprintf("Host %q", fixture.ServerHostName),
Numeric: false,
Rank: 1,
Rows: []render.Row{
{Key: "Load (1m)", ValueMajor: "0.01", Metric: &fixture.LoadMetric, ValueType: "sparkline"},
{Key: "Load (5m)", ValueMajor: "0.01", Metric: &fixture.LoadMetric, ValueType: "sparkline"},
{Key: "Load (15m)", ValueMajor: "0.01", Metric: &fixture.LoadMetric, ValueType: "sparkline"},
{Key: "Operating system", ValueMajor: "Linux"},
},
},
{
Title: "Connections",
Numeric: false,
Rank: 0,
Rows: []render.Row{
{Key: "Ingress packet rate", ValueMajor: "105", ValueMinor: "packets/sec"},
{Key: "Ingress byte rate", ValueMajor: "1.0", ValueMinor: "KBps"},
{Key: "Client", ValueMajor: "Server", Expandable: true},
{
Key: fmt.Sprintf("%s:%s", fixture.UnknownClient1IP, fixture.UnknownClient1Port),
ValueMajor: fmt.Sprintf("%s:%s", fixture.ServerIP, fixture.ServerPort),
Expandable: true,
},
{
Key: fmt.Sprintf("%s:%s", fixture.UnknownClient2IP, fixture.UnknownClient2Port),
ValueMajor: fmt.Sprintf("%s:%s", fixture.ServerIP, fixture.ServerPort),
Expandable: true,
},
{
Key: fmt.Sprintf("%s:%s", fixture.UnknownClient3IP, fixture.UnknownClient3Port),
ValueMajor: fmt.Sprintf("%s:%s", fixture.ServerIP, fixture.ServerPort),
Expandable: true,
},
{
Key: fmt.Sprintf("%s:%s", fixture.ClientIP, fixture.ClientPort54001),
ValueMajor: fmt.Sprintf("%s:%s", fixture.ServerIP, fixture.ServerPort),
Expandable: true,
},
{
Key: fmt.Sprintf("%s:%s", fixture.ClientIP, fixture.ClientPort54002),
ValueMajor: fmt.Sprintf("%s:%s", fixture.ServerIP, fixture.ServerPort),
Expandable: true,
},
{
Key: fmt.Sprintf("%s:%s", fixture.RandomClientIP, fixture.RandomClientPort),
ValueMajor: fmt.Sprintf("%s:%s", fixture.ServerIP, fixture.ServerPort),
Expandable: true,
},
},
},
},
}
if !reflect.DeepEqual(want, have) {
t.Errorf("%s", test.Diff(want, have))
}
}
+111 -170
View File
@@ -23,10 +23,6 @@ var (
EgressPacketCount: newu64(70),
EgressByteCount: newu64(700),
},
Origins: report.MakeIDList(
fixture.UnknownClient1NodeID,
fixture.UnknownClient2NodeID,
),
}
}
unknownPseudoNode2 = func(adjacent string) render.RenderableNode {
@@ -39,9 +35,6 @@ var (
EgressPacketCount: newu64(50),
EgressByteCount: newu64(500),
},
Origins: report.MakeIDList(
fixture.UnknownClient3NodeID,
),
}
}
theInternetNode = func(adjacent string) render.RenderableNode {
@@ -54,10 +47,6 @@ var (
EgressPacketCount: newu64(60),
EgressByteCount: newu64(600),
},
Origins: report.MakeIDList(
fixture.RandomClientNodeID,
fixture.GoogleEndpointNodeID,
),
}
}
ClientProcess1ID = render.MakeProcessID(fixture.ClientHostID, fixture.Client1PID)
@@ -72,12 +61,7 @@ var (
LabelMinor: fmt.Sprintf("%s (%s)", fixture.ClientHostID, fixture.Client1PID),
Rank: fixture.Client1Name,
Pseudo: false,
Origins: report.MakeIDList(
fixture.Client54001NodeID,
fixture.ClientProcess1NodeID,
fixture.ClientHostNodeID,
),
Node: report.MakeNode().WithAdjacent(ServerProcessID),
Node: report.MakeNode().WithAdjacent(ServerProcessID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(10),
EgressByteCount: newu64(100),
@@ -89,12 +73,7 @@ var (
LabelMinor: fmt.Sprintf("%s (%s)", fixture.ClientHostID, fixture.Client2PID),
Rank: fixture.Client2Name,
Pseudo: false,
Origins: report.MakeIDList(
fixture.Client54002NodeID,
fixture.ClientProcess2NodeID,
fixture.ClientHostNodeID,
),
Node: report.MakeNode().WithAdjacent(ServerProcessID),
Node: report.MakeNode().WithAdjacent(ServerProcessID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(20),
EgressByteCount: newu64(200),
@@ -106,28 +85,18 @@ var (
LabelMinor: fmt.Sprintf("%s (%s)", fixture.ServerHostID, fixture.ServerPID),
Rank: fixture.ServerName,
Pseudo: false,
Origins: report.MakeIDList(
fixture.Server80NodeID,
fixture.ServerProcessNodeID,
fixture.ServerHostNodeID,
),
Node: report.MakeNode(),
Node: report.MakeNode(),
EdgeMetadata: report.EdgeMetadata{
IngressPacketCount: newu64(210),
IngressByteCount: newu64(2100),
},
},
nonContainerProcessID: {
ID: nonContainerProcessID,
LabelMajor: fixture.NonContainerName,
LabelMinor: fmt.Sprintf("%s (%s)", fixture.ServerHostID, fixture.NonContainerPID),
Rank: fixture.NonContainerName,
Pseudo: false,
Origins: report.MakeIDList(
fixture.NonContainerProcessNodeID,
fixture.ServerHostNodeID,
fixture.NonContainerNodeID,
),
ID: nonContainerProcessID,
LabelMajor: fixture.NonContainerName,
LabelMinor: fmt.Sprintf("%s (%s)", fixture.ServerHostID, fixture.NonContainerPID),
Rank: fixture.NonContainerName,
Pseudo: false,
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
},
@@ -136,6 +105,10 @@ var (
render.TheInternetID: theInternetNode(ServerProcessID),
}).Prune()
ServerProcessRenderedID = render.MakeProcessID(fixture.ServerHostID, fixture.ServerPID)
ClientProcess1RenderedID = render.MakeProcessID(fixture.ClientHostID, fixture.Client1PID)
ClientProcess2RenderedID = render.MakeProcessID(fixture.ClientHostID, fixture.Client2PID)
RenderedProcessNames = (render.RenderableNodes{
fixture.Client1Name: {
ID: fixture.Client1Name,
@@ -143,12 +116,9 @@ var (
LabelMinor: "2 processes",
Rank: fixture.Client1Name,
Pseudo: false,
Origins: report.MakeIDList(
fixture.Client54001NodeID,
fixture.Client54002NodeID,
fixture.ClientProcess1NodeID,
fixture.ClientProcess2NodeID,
fixture.ClientHostNodeID,
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID],
),
Node: report.MakeNode().WithAdjacent(fixture.ServerName),
EdgeMetadata: report.EdgeMetadata{
@@ -162,10 +132,8 @@ var (
LabelMinor: "1 process",
Rank: fixture.ServerName,
Pseudo: false,
Origins: report.MakeIDList(
fixture.Server80NodeID,
fixture.ServerProcessNodeID,
fixture.ServerHostNodeID,
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.ServerProcessNodeID],
),
Node: report.MakeNode(),
EdgeMetadata: report.EdgeMetadata{
@@ -179,10 +147,8 @@ var (
LabelMinor: "1 process",
Rank: fixture.NonContainerName,
Pseudo: false,
Origins: report.MakeIDList(
fixture.NonContainerProcessNodeID,
fixture.ServerHostNodeID,
fixture.NonContainerNodeID,
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID],
),
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
@@ -192,41 +158,35 @@ var (
render.TheInternetID: theInternetNode(fixture.ServerName),
}).Prune()
ServerContainerRenderedID = render.MakeContainerID(fixture.ServerContainerID)
ClientContainerRenderedID = render.MakeContainerID(fixture.ClientContainerID)
RenderedContainers = (render.RenderableNodes{
fixture.ClientContainerID: {
ID: fixture.ClientContainerID,
ClientContainerRenderedID: {
ID: ClientContainerRenderedID,
LabelMajor: "client",
LabelMinor: fixture.ClientHostName,
Rank: fixture.ClientContainerImageName,
Pseudo: false,
Origins: report.MakeIDList(
fixture.ClientContainerImageNodeID,
fixture.ClientContainerNodeID,
fixture.Client54001NodeID,
fixture.Client54002NodeID,
fixture.ClientProcess1NodeID,
fixture.ClientProcess2NodeID,
fixture.ClientHostNodeID,
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID],
),
Node: report.MakeNode().WithAdjacent(fixture.ServerContainerID),
Node: report.MakeNode().WithAdjacent(ServerContainerRenderedID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(30),
EgressByteCount: newu64(300),
},
ControlNode: fixture.ClientContainerNodeID,
},
fixture.ServerContainerID: {
ID: fixture.ServerContainerID,
ServerContainerRenderedID: {
ID: ServerContainerRenderedID,
LabelMajor: "server",
LabelMinor: fixture.ServerHostName,
Rank: fixture.ServerContainerImageName,
Pseudo: false,
Origins: report.MakeIDList(
fixture.ServerContainerImageNodeID,
fixture.ServerContainerNodeID,
fixture.Server80NodeID,
fixture.ServerProcessNodeID,
fixture.ServerHostNodeID,
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.ServerProcessNodeID],
),
Node: report.MakeNode(),
EdgeMetadata: report.EdgeMetadata{
@@ -241,51 +201,46 @@ var (
LabelMinor: fixture.ServerHostName,
Rank: "",
Pseudo: true,
Origins: report.MakeIDList(
fixture.NonContainerProcessNodeID,
fixture.ServerHostNodeID,
fixture.NonContainerNodeID,
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID],
),
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
},
render.TheInternetID: theInternetNode(fixture.ServerContainerID),
render.TheInternetID: theInternetNode(ServerContainerRenderedID),
}).Prune()
ClientContainerImageRenderedName = render.MakeContainerImageID(fixture.ClientContainerImageName)
ServerContainerImageRenderedName = render.MakeContainerImageID(fixture.ServerContainerImageName)
RenderedContainerImages = (render.RenderableNodes{
fixture.ClientContainerImageName: {
ID: fixture.ClientContainerImageName,
ClientContainerImageRenderedName: {
ID: ClientContainerImageRenderedName,
LabelMajor: fixture.ClientContainerImageName,
LabelMinor: "1 container",
Rank: fixture.ClientContainerImageName,
Pseudo: false,
Origins: report.MakeIDList(
fixture.ClientContainerImageNodeID,
fixture.ClientContainerNodeID,
fixture.Client54001NodeID,
fixture.Client54002NodeID,
fixture.ClientProcess1NodeID,
fixture.ClientProcess2NodeID,
fixture.ClientHostNodeID,
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID],
fixture.Report.Container.Nodes[fixture.ClientContainerNodeID],
),
Node: report.MakeNode().WithAdjacent(fixture.ServerContainerImageName),
Node: report.MakeNode().WithAdjacent(ServerContainerImageRenderedName),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(30),
EgressByteCount: newu64(300),
},
},
fixture.ServerContainerImageName: {
ID: fixture.ServerContainerImageName,
ServerContainerImageRenderedName: {
ID: ServerContainerImageRenderedName,
LabelMajor: fixture.ServerContainerImageName,
LabelMinor: "1 container",
Rank: fixture.ServerContainerImageName,
Pseudo: false,
Origins: report.MakeIDList(
fixture.ServerContainerImageNodeID,
fixture.ServerContainerNodeID,
fixture.Server80NodeID,
fixture.ServerProcessNodeID,
fixture.ServerHostNodeID),
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.ServerProcessNodeID],
fixture.Report.Container.Nodes[fixture.ServerContainerNodeID],
),
Node: report.MakeNode(),
EdgeMetadata: report.EdgeMetadata{
IngressPacketCount: newu64(210),
@@ -298,15 +253,13 @@ var (
LabelMinor: fixture.ServerHostName,
Rank: "",
Pseudo: true,
Origins: report.MakeIDList(
fixture.NonContainerNodeID,
fixture.NonContainerProcessNodeID,
fixture.ServerHostNodeID,
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID],
),
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
},
render.TheInternetID: theInternetNode(fixture.ServerContainerImageName),
render.TheInternetID: theInternetNode(ServerContainerImageRenderedName),
}).Prune()
ServerHostRenderedID = render.MakeHostID(fixture.ServerHostID)
@@ -321,13 +274,15 @@ var (
LabelMinor: "hostname.com", // after first .
Rank: "hostname.com",
Pseudo: false,
Origins: report.MakeIDList(
fixture.ServerHostNodeID,
fixture.ServerAddressNodeID,
Children: report.MakeNodeSet(
fixture.Report.Container.Nodes[fixture.ServerContainerNodeID],
fixture.Report.Container.Nodes[fixture.ServerProcessNodeID],
),
Node: report.MakeNode(),
EdgeMetadata: report.EdgeMetadata{
MaxConnCountTCP: newu64(3),
IngressPacketCount: newu64(210),
IngressByteCount: newu64(2100),
MaxConnCountTCP: newu64(3),
},
},
ClientHostRenderedID: {
@@ -336,13 +291,16 @@ var (
LabelMinor: "hostname.com", // after first .
Rank: "hostname.com",
Pseudo: false,
Origins: report.MakeIDList(
fixture.ClientHostNodeID,
fixture.ClientAddressNodeID,
Children: report.MakeNodeSet(
fixture.Report.Container.Nodes[fixture.ClientContainerNodeID],
fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID],
),
Node: report.MakeNode().WithAdjacent(ServerHostRenderedID),
EdgeMetadata: report.EdgeMetadata{
MaxConnCountTCP: newu64(3),
EgressPacketCount: newu64(30),
EgressByteCount: newu64(300),
MaxConnCountTCP: newu64(3),
},
},
pseudoHostID1: {
@@ -351,7 +309,10 @@ var (
Pseudo: true,
Node: report.MakeNode().WithAdjacent(ServerHostRenderedID),
EdgeMetadata: report.EdgeMetadata{},
Origins: report.MakeIDList(fixture.UnknownAddress1NodeID, fixture.UnknownAddress2NodeID),
Children: report.MakeNodeSet(
fixture.Report.Container.Nodes[fixture.ServerContainerNodeID],
fixture.Report.Process.Nodes[fixture.ServerProcessNodeID],
),
},
pseudoHostID2: {
ID: pseudoHostID2,
@@ -359,7 +320,6 @@ var (
Pseudo: true,
Node: report.MakeNode().WithAdjacent(ServerHostRenderedID),
EdgeMetadata: report.EdgeMetadata{},
Origins: report.MakeIDList(fixture.UnknownAddress3NodeID),
},
render.TheInternetID: {
ID: render.TheInternetID,
@@ -367,46 +327,43 @@ var (
Pseudo: true,
Node: report.MakeNode().WithAdjacent(ServerHostRenderedID),
EdgeMetadata: report.EdgeMetadata{},
Origins: report.MakeIDList(fixture.RandomAddressNodeID),
},
}).Prune()
ClientPodRenderedID = render.MakePodID("ping/pong-a")
ServerPodRenderedID = render.MakePodID("ping/pong-b")
RenderedPods = (render.RenderableNodes{
"ping/pong-a": {
ID: "ping/pong-a",
ClientPodRenderedID: {
ID: ClientPodRenderedID,
LabelMajor: "pong-a",
LabelMinor: "1 container",
Rank: "ping/pong-a",
Pseudo: false,
Origins: report.MakeIDList(
fixture.Client54001NodeID,
fixture.Client54002NodeID,
fixture.ClientProcess1NodeID,
fixture.ClientProcess2NodeID,
fixture.ClientHostNodeID,
fixture.ClientContainerNodeID,
fixture.ClientContainerImageNodeID,
fixture.ClientPodNodeID,
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID],
fixture.Report.Container.Nodes[fixture.ClientContainerNodeID],
fixture.Report.ContainerImage.Nodes[fixture.ClientContainerImageNodeID],
fixture.Report.Pod.Nodes[fixture.ClientPodNodeID],
),
Node: report.MakeNode().WithAdjacent("ping/pong-b"),
Node: report.MakeNode().WithAdjacent(ServerPodRenderedID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(30),
EgressByteCount: newu64(300),
},
},
"ping/pong-b": {
ID: "ping/pong-b",
ServerPodRenderedID: {
ID: ServerPodRenderedID,
LabelMajor: "pong-b",
LabelMinor: "1 container",
Rank: "ping/pong-b",
Pseudo: false,
Origins: report.MakeIDList(
fixture.Server80NodeID,
fixture.ServerPodNodeID,
fixture.ServerProcessNodeID,
fixture.ServerContainerNodeID,
fixture.ServerHostNodeID,
fixture.ServerContainerImageNodeID,
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.ServerProcessNodeID],
fixture.Report.Container.Nodes[fixture.ServerContainerNodeID],
fixture.Report.ContainerImage.Nodes[fixture.ServerContainerImageNodeID],
fixture.Report.Pod.Nodes[fixture.ServerPodNodeID],
),
Node: report.MakeNode(),
EdgeMetadata: report.EdgeMetadata{
@@ -420,10 +377,8 @@ var (
LabelMinor: fixture.ServerHostName,
Rank: "",
Pseudo: true,
Origins: report.MakeIDList(
fixture.ServerHostNodeID,
fixture.NonContainerProcessNodeID,
fixture.NonContainerNodeID,
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID],
),
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
@@ -432,43 +387,35 @@ var (
ID: render.TheInternetID,
LabelMajor: render.TheInternetMajor,
Pseudo: true,
Node: report.MakeNode().WithAdjacent("ping/pong-b"),
Node: report.MakeNode().WithAdjacent(ServerPodRenderedID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(60),
EgressByteCount: newu64(600),
},
Origins: report.MakeIDList(
fixture.RandomClientNodeID,
fixture.GoogleEndpointNodeID,
),
},
}).Prune()
ServiceRenderedID = render.MakeServiceID("ping/pongservice")
RenderedPodServices = (render.RenderableNodes{
"ping/pongservice": {
ID: fixture.ServiceID,
ServiceRenderedID: {
ID: ServiceRenderedID,
LabelMajor: "pongservice",
LabelMinor: "2 pods",
Rank: fixture.ServiceID,
Pseudo: false,
Origins: report.MakeIDList(
fixture.Client54001NodeID,
fixture.Client54002NodeID,
fixture.ClientProcess1NodeID,
fixture.ClientProcess2NodeID,
fixture.ClientHostNodeID,
fixture.ClientContainerNodeID,
fixture.ClientContainerImageNodeID,
fixture.ClientPodNodeID,
fixture.Server80NodeID,
fixture.ServerPodNodeID,
fixture.ServiceNodeID,
fixture.ServerProcessNodeID,
fixture.ServerContainerNodeID,
fixture.ServerHostNodeID,
fixture.ServerContainerImageNodeID,
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.ClientProcess1NodeID],
fixture.Report.Process.Nodes[fixture.ClientProcess2NodeID],
fixture.Report.Container.Nodes[fixture.ClientContainerNodeID],
fixture.Report.ContainerImage.Nodes[fixture.ClientContainerImageNodeID],
fixture.Report.Pod.Nodes[fixture.ClientPodNodeID],
fixture.Report.Process.Nodes[fixture.ServerProcessNodeID],
fixture.Report.Container.Nodes[fixture.ServerContainerNodeID],
fixture.Report.ContainerImage.Nodes[fixture.ServerContainerImageNodeID],
fixture.Report.Pod.Nodes[fixture.ServerPodNodeID],
),
Node: report.MakeNode().WithAdjacent(fixture.ServiceID), // ?? Shouldn't be adjacent to itself?
Node: report.MakeNode().WithAdjacent(ServiceRenderedID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(30),
EgressByteCount: newu64(300),
@@ -482,10 +429,8 @@ var (
LabelMinor: fixture.ServerHostName,
Rank: "",
Pseudo: true,
Origins: report.MakeIDList(
fixture.ServerHostNodeID,
fixture.NonContainerProcessNodeID,
fixture.NonContainerNodeID,
Children: report.MakeNodeSet(
fixture.Report.Process.Nodes[fixture.NonContainerProcessNodeID],
),
Node: report.MakeNode().WithAdjacent(render.TheInternetID),
EdgeMetadata: report.EdgeMetadata{},
@@ -494,15 +439,11 @@ var (
ID: render.TheInternetID,
LabelMajor: render.TheInternetMajor,
Pseudo: true,
Node: report.MakeNode().WithAdjacent(fixture.ServiceID),
Node: report.MakeNode().WithAdjacent(ServiceRenderedID),
EdgeMetadata: report.EdgeMetadata{
EgressPacketCount: newu64(60),
EgressByteCount: newu64(600),
},
Origins: report.MakeIDList(
fixture.RandomClientNodeID,
fixture.GoogleEndpointNodeID,
),
},
}).Prune()
)
+11
View File
@@ -119,6 +119,17 @@ func (f Filter) Stats(rpt report.Report) Stats {
// to indicate a node has an edge pointing to it or from it
const IsConnected = "is_connected"
// FilterPseudo produces a renderer that removes pseudo nodes from the given
// renderer
func FilterPseudo(r Renderer) Renderer {
return Filter{
Renderer: r,
FilterFunc: func(node RenderableNode) bool {
return !node.Pseudo
},
}
}
// FilterUnconnected produces a renderer that filters unconnected nodes
// from the given renderer
func FilterUnconnected(r Renderer) Renderer {
+19 -1
View File
@@ -48,7 +48,7 @@ func TestFilterRender2(t *testing.T) {
}
}
func TestFilterUnconnectedPesudoNodes(t *testing.T) {
func TestFilterUnconnectedPseudoNodes(t *testing.T) {
// Test pseudo nodes that are made unconnected by filtering
// are also removed.
{
@@ -123,3 +123,21 @@ func TestFilterUnconnectedSelf(t *testing.T) {
}
}
}
func TestFilterPseudo(t *testing.T) {
// Test pseudonodes are removed
{
nodes := render.RenderableNodes{
"foo": {ID: "foo", Node: report.MakeNode()},
"bar": {ID: "bar", Pseudo: true, Node: report.MakeNode()},
}
renderer := render.FilterPseudo(mockRenderer{RenderableNodes: nodes})
want := render.RenderableNodes{
"foo": {ID: "foo", Node: report.MakeNode()},
}
have := renderer.Render(report.MakeReport()).Prune()
if !reflect.DeepEqual(want, have) {
t.Error(test.Diff(want, have))
}
}
}
+30 -6
View File
@@ -1,32 +1,56 @@
package render
import (
"fmt"
"strings"
)
// makeID is the generic ID maker
func makeID(prefix string, parts ...string) string {
return strings.Join(append([]string{prefix}, parts...), ":")
}
// MakeEndpointID makes an endpoint node ID for rendered nodes.
func MakeEndpointID(hostID, addr, port string) string {
return fmt.Sprintf("endpoint:%s:%s:%s", hostID, addr, port)
return makeID("endpoint", hostID, addr, port)
}
// MakeProcessID makes a process node ID for rendered nodes.
func MakeProcessID(hostID, pid string) string {
return fmt.Sprintf("process:%s:%s", hostID, pid)
return makeID("process", hostID, pid)
}
// MakeAddressID makes an address node ID for rendered nodes.
func MakeAddressID(hostID, addr string) string {
return fmt.Sprintf("address:%s:%s", hostID, addr)
return makeID("address", hostID, addr)
}
// MakeContainerID makes a container node ID for rendered nodes.
func MakeContainerID(containerID string) string {
return makeID("container", containerID)
}
// MakeContainerImageID makes a container image node ID for rendered nodes.
func MakeContainerImageID(imageID string) string {
return makeID("container_image", imageID)
}
// MakePodID makes a pod node ID for rendered nodes.
func MakePodID(podID string) string {
return makeID("pod", podID)
}
// MakeServiceID makes a service node ID for rendered nodes.
func MakeServiceID(serviceID string) string {
return makeID("service", serviceID)
}
// MakeHostID makes a host node ID for rendered nodes.
func MakeHostID(hostID string) string {
return fmt.Sprintf("host:%s", hostID)
return makeID("host", hostID)
}
// MakePseudoNodeID produces a pseudo node ID from its composite parts,
// for use in rendered nodes.
func MakePseudoNodeID(parts ...string) string {
return strings.Join(append([]string{"pseudo"}, parts...), ":")
return makeID("pseudo", parts...)
}
+89 -42
View File
@@ -123,12 +123,13 @@ func MapProcessIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
// renderable node. As it is only ever run on container topology nodes, we
// expect that certain keys are present.
func MapContainerIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
id, ok := m.Metadata[docker.ContainerID]
containerID, ok := m.Metadata[docker.ContainerID]
if !ok {
return RenderableNodes{}
}
var (
id = MakeContainerID(containerID)
major, _ = GetRenderableContainerName(m.Node)
minor = report.ExtractHostID(m.Node)
rank = m.Metadata[docker.ImageID]
@@ -136,10 +137,6 @@ func MapContainerIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
node := NewRenderableNodeWith(id, major, minor, rank, m)
node.ControlNode = m.ID
if imageID, ok := m.Metadata[docker.ImageID]; ok {
hostID, _, _ := report.ParseContainerNodeID(m.ID)
node.Origins = node.Origins.Add(report.MakeContainerNodeID(hostID, imageID))
}
return RenderableNodes{id: node}
}
@@ -171,14 +168,15 @@ func GetRenderableContainerName(nmd report.Node) (string, bool) {
// image renderable node. As it is only ever run on container image topology
// nodes, we expect that certain keys are present.
func MapContainerImageIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
id, ok := m.Metadata[docker.ImageID]
imageID, ok := m.Metadata[docker.ImageID]
if !ok {
return RenderableNodes{}
}
var (
id = MakeContainerImageID(imageID)
major = m.Metadata[docker.ImageName]
rank = m.Metadata[docker.ImageID]
rank = imageID
)
return RenderableNodes{id: NewRenderableNodeWith(id, major, "", rank, m)}
@@ -188,12 +186,13 @@ func MapContainerImageIdentity(m RenderableNode, _ report.Networks) RenderableNo
// only ever run on pod topology nodes, we expect that certain keys
// are present.
func MapPodIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
id, ok := m.Metadata[kubernetes.PodID]
podID, ok := m.Metadata[kubernetes.PodID]
if !ok {
return RenderableNodes{}
}
var (
id = MakePodID(podID)
major = m.Metadata[kubernetes.PodName]
rank = m.Metadata[kubernetes.PodID]
)
@@ -205,12 +204,13 @@ func MapPodIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
// only ever run on service topology nodes, we expect that certain keys
// are present.
func MapServiceIdentity(m RenderableNode, _ report.Networks) RenderableNodes {
id, ok := m.Metadata[kubernetes.ServiceID]
serviceID, ok := m.Metadata[kubernetes.ServiceID]
if !ok {
return RenderableNodes{}
}
var (
id = MakeServiceID(serviceID)
major = m.Metadata[kubernetes.ServiceName]
rank = m.Metadata[kubernetes.ServiceID]
)
@@ -308,6 +308,7 @@ func MapEndpoint2IP(m RenderableNode, local report.Networks) RenderableNodes {
// So we need to emit two nodes, for two different cases.
id := report.MakeScopedEndpointNodeID(scope, addr, "")
idWithPort := report.MakeScopedEndpointNodeID(scope, addr, port)
m = m.WithParents(nil)
return RenderableNodes{
id: NewRenderableNodeWith(id, "", "", "", m),
idWithPort: NewRenderableNodeWith(idWithPort, "", "", "", m),
@@ -340,7 +341,7 @@ func MapContainer2IP(m RenderableNode, _ report.Networks) RenderableNodes {
if mapping := portMappingMatch.FindStringSubmatch(portMapping); mapping != nil {
ip, port := mapping[1], mapping[2]
id := report.MakeScopedEndpointNodeID("", ip, port)
node := NewRenderableNodeWith(id, "", "", "", m)
node := NewRenderableNodeWith(id, "", "", "", m.WithParents(nil))
node.Counters[containersKey] = 1
result[id] = node
}
@@ -367,12 +368,14 @@ func MapIP2Container(n RenderableNode, _ report.Networks) RenderableNodes {
// If this node is not a container, exclude it.
// This excludes all the nodes we've dragged in from endpoint
// that we failed to join to a container.
id, ok := n.Node.Metadata[docker.ContainerID]
containerID, ok := n.Node.Metadata[docker.ContainerID]
if !ok {
return RenderableNodes{}
}
return RenderableNodes{id: NewDerivedNode(id, n)}
id := MakeContainerID(containerID)
return RenderableNodes{id: NewDerivedNode(id, n.WithParents(nil))}
}
// MapEndpoint2Process maps endpoint RenderableNodes to process
@@ -397,7 +400,7 @@ func MapEndpoint2Process(n RenderableNode, _ report.Networks) RenderableNodes {
}
id := MakeProcessID(report.ExtractHostID(n.Node), pid)
return RenderableNodes{id: NewDerivedNode(id, n)}
return RenderableNodes{id: NewDerivedNode(id, n.WithParents(nil))}
}
// MapProcess2Container maps process RenderableNodes to container
@@ -426,16 +429,23 @@ func MapProcess2Container(n RenderableNode, _ report.Networks) RenderableNodes {
// into an per-host "Uncontained" node. If for whatever reason
// this node doesn't have a host id in their nodemetadata, it'll
// all get grouped into a single uncontained node.
id, ok := n.Node.Metadata[docker.ContainerID]
if !ok {
hostID := report.ExtractHostID(n.Node)
var (
id string
node RenderableNode
hostID = report.ExtractHostID(n.Node)
)
n = n.WithParents(nil)
if containerID, ok := n.Node.Metadata[docker.ContainerID]; ok {
id = MakeContainerID(containerID)
node = NewDerivedNode(id, n)
} else {
id = MakePseudoNodeID(UncontainedID, hostID)
node := newDerivedPseudoNode(id, UncontainedMajor, n)
node = newDerivedPseudoNode(id, UncontainedMajor, n)
node.LabelMinor = hostID
return RenderableNodes{id: node}
}
return RenderableNodes{id: NewDerivedNode(id, n)}
node.Children = node.Children.Add(n.Node)
return RenderableNodes{id: node}
}
// MapProcess2Name maps process RenderableNodes to RenderableNodes
@@ -458,6 +468,9 @@ func MapProcess2Name(n RenderableNode, _ report.Networks) RenderableNodes {
node.LabelMajor = name
node.Rank = name
node.Node.Counters[processesKey] = 1
node.Node.Topology = "process_name"
node.Node.ID = name
node.Children = node.Children.Add(n.Node)
return RenderableNodes{name: node}
}
@@ -497,14 +510,21 @@ func MapContainer2ContainerImage(n RenderableNode, _ report.Networks) Renderable
// 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.Node.Metadata[docker.ImageID]
imageID, ok := n.Node.Metadata[docker.ImageID]
if !ok {
return RenderableNodes{}
}
// Add container id key to the counters, which will later be counted to produce the minor label
result := NewDerivedNode(id, n)
id := MakeContainerImageID(imageID)
result := NewDerivedNode(id, n.WithParents(nil))
result.Node.Counters[containersKey] = 1
// Add the container as a child of the new image node
result.Children = result.Children.Add(n.Node)
result.Node.Topology = "container_image"
result.Node.ID = report.MakeContainerImageNodeID(report.ExtractHostID(n.Node), imageID)
return RenderableNodes{id: result}
}
@@ -532,15 +552,19 @@ func MapPod2Service(n RenderableNode, _ report.Networks) RenderableNodes {
}
result := RenderableNodes{}
for _, id := range strings.Fields(ids) {
n := NewDerivedNode(id, n)
for _, serviceID := range strings.Fields(ids) {
id := MakeServiceID(serviceID)
n := NewDerivedNode(id, n.WithParents(nil))
n.Node.Counters[podsKey] = 1
n.Children = n.Children.Add(n.Node)
result[id] = n
}
return result
}
func imageNameWithoutVersion(name string) string {
// ImageNameWithoutVersion splits the image name apart, returning the name
// without the version, if possible
func ImageNameWithoutVersion(name string) string {
parts := strings.SplitN(name, "/", 3)
if len(parts) == 3 {
name = fmt.Sprintf("%s/%s", parts[1], parts[2])
@@ -565,13 +589,38 @@ func MapContainerImage2Name(n RenderableNode, _ report.Networks) RenderableNodes
return RenderableNodes{}
}
name = imageNameWithoutVersion(name)
name = ImageNameWithoutVersion(name)
id := MakeContainerImageID(name)
node := NewDerivedNode(name, n)
node := NewDerivedNode(id, n)
node.LabelMajor = name
node.Rank = name
node.Node = n.Node.Copy() // Propagate NMD for container counting.
return RenderableNodes{name: node}
return RenderableNodes{id: node}
}
// MapX2Host maps any RenderableNodes to host
// RenderableNodes.
//
// If this function is given a node without a hostname
// (including other pseudo nodes), it will drop the 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 MapX2Host(n RenderableNode, _ report.Networks) RenderableNodes {
// Propogate all pseudo nodes
if n.Pseudo {
return RenderableNodes{n.ID: n}
}
if _, ok := n.Node.Metadata[report.HostNodeID]; !ok {
return RenderableNodes{}
}
id := MakeHostID(report.ExtractHostID(n.Node))
result := NewDerivedNode(id, n.WithParents(nil))
result.Children = result.Children.Add(n.Node)
return RenderableNodes{id: result}
}
// MapContainer2Pod maps container RenderableNodes to pod
@@ -593,23 +642,27 @@ func MapContainer2Pod(n RenderableNode, _ report.Networks) RenderableNodes {
// Otherwise, if some some reason the container doesn't have a pod_id (maybe
// slightly out of sync reports, or its not in a pod), just drop it
id, ok := n.Node.Metadata["docker_label_io.kubernetes.pod.name"]
podID, ok := n.Node.Metadata[kubernetes.PodID]
if !ok {
return RenderableNodes{}
}
id := MakePodID(podID)
// Add container-<id> key to NMD, which will later be counted to produce the
// minor label
result := NewRenderableNodeWith(id, "", "", id, n)
result := NewRenderableNodeWith(id, "", "", podID, n.WithParents(nil))
result.Node.Counters[containersKey] = 1
// Due to a bug in kubernetes, addon pods on the master node are not returned
// from the API. This is a workaround until
// https://github.com/kubernetes/kubernetes/issues/14738 is fixed.
if s := strings.SplitN(id, "/", 2); len(s) == 2 {
if s := strings.SplitN(podID, "/", 2); len(s) == 2 {
result.LabelMajor = s[1]
result.Node.Metadata[kubernetes.Namespace] = s[0]
result.Node.Metadata[kubernetes.PodName] = s[1]
}
result.Children = result.Children.Add(n.Node)
return RenderableNodes{id: result}
}
@@ -633,6 +686,12 @@ func MapContainer2Hostname(n RenderableNode, _ report.Networks) RenderableNodes
// Add container id key to the counters, which will later be counted to produce the minor label
result.Node.Counters[containersKey] = 1
result.Node.Topology = "container_hostname"
result.Node.ID = id
result.Children = result.Children.Add(n.Node)
return RenderableNodes{id: result}
}
@@ -669,18 +728,6 @@ func MapCountPods(n RenderableNode, _ report.Networks) RenderableNodes {
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, _ report.Networks) RenderableNodes {
if n.Pseudo {
return RenderableNodes{n.ID: n}
}
id := MakeHostID(report.ExtractHostID(n.Node))
return RenderableNodes{id: NewDerivedNode(id, n)}
}
// trySplitAddr is basically ParseArbitraryNodeID, since its callsites
// (pseudo funcs) just have opaque node IDs and don't know what topology they
// come from. Without changing how pseudo funcs work, we can't make it much
+1 -1
View File
@@ -12,7 +12,7 @@ func TestDockerImageName(t *testing.T) {
{"docker-registry.domain.name:5000/repo/image1:ver", "repo/image1"},
{"foo", "foo"},
} {
name := imageNameWithoutVersion(input.in)
name := ImageNameWithoutVersion(input.in)
if name != input.name {
t.Fatalf("%s: %s != %s", input.in, name, input.name)
}
+23 -16
View File
@@ -8,13 +8,13 @@ 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
Origins report.IDList `json:"origins,omitempty"` // Core node IDs that contributed information
ControlNode string `json:"-"` // ID of node from which to show the controls in the UI
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
Children report.NodeSet `json:"children,omitempty"` // Nodes which have been grouped into this one
ControlNode string `json:"-"` // ID of node from which to show the controls in the UI
report.EdgeMetadata `json:"metadata"` // Numeric sums
report.Node
@@ -28,23 +28,22 @@ func NewRenderableNode(id string) RenderableNode {
LabelMinor: "",
Rank: "",
Pseudo: false,
Origins: report.MakeIDList(),
EdgeMetadata: report.EdgeMetadata{},
Node: report.MakeNode(),
}
}
// NewRenderableNodeWith makes a new RenderableNode with some fields filled in
func NewRenderableNodeWith(id, major, minor, rank string, rn RenderableNode) RenderableNode {
func NewRenderableNodeWith(id, major, minor, rank string, node RenderableNode) RenderableNode {
return RenderableNode{
ID: id,
LabelMajor: major,
LabelMinor: minor,
Rank: rank,
Pseudo: false,
Origins: rn.Origins.Copy(),
EdgeMetadata: rn.EdgeMetadata.Copy(),
Node: rn.Node.Copy(),
Children: node.Children.Copy(),
EdgeMetadata: node.EdgeMetadata.Copy(),
Node: node.Node.Copy(),
}
}
@@ -56,7 +55,7 @@ func NewDerivedNode(id string, node RenderableNode) RenderableNode {
LabelMinor: "",
Rank: "",
Pseudo: node.Pseudo,
Origins: node.Origins.Copy(),
Children: node.Children.Copy(),
EdgeMetadata: node.EdgeMetadata.Copy(),
Node: node.Node.Copy(),
ControlNode: "", // Do not propagate ControlNode when making a derived node!
@@ -70,7 +69,7 @@ func newDerivedPseudoNode(id, major string, node RenderableNode) RenderableNode
LabelMinor: "",
Rank: "",
Pseudo: true,
Origins: node.Origins.Copy(),
Children: node.Children.Copy(),
EdgeMetadata: node.EdgeMetadata.Copy(),
Node: node.Node.Copy(),
}
@@ -83,6 +82,13 @@ func (rn RenderableNode) WithNode(n report.Node) RenderableNode {
return result
}
// WithParents creates a new RenderableNode based on rn, where n has the given parents set
func (rn RenderableNode) WithParents(p report.Sets) RenderableNode {
result := rn.Copy()
result.Node.Parents = p
return result
}
// Merge merges rn with other and returns a new RenderableNode
func (rn RenderableNode) Merge(other RenderableNode) RenderableNode {
result := rn.Copy()
@@ -107,7 +113,7 @@ func (rn RenderableNode) Merge(other RenderableNode) RenderableNode {
panic(result.ID)
}
result.Origins = rn.Origins.Merge(other.Origins)
result.Children = rn.Children.Merge(other.Children)
result.EdgeMetadata = rn.EdgeMetadata.Merge(other.EdgeMetadata)
result.Node = rn.Node.Merge(other.Node)
@@ -122,7 +128,7 @@ func (rn RenderableNode) Copy() RenderableNode {
LabelMinor: rn.LabelMinor,
Rank: rn.Rank,
Pseudo: rn.Pseudo,
Origins: rn.Origins.Copy(),
Children: rn.Children.Copy(),
EdgeMetadata: rn.EdgeMetadata.Copy(),
Node: rn.Node.Copy(),
ControlNode: rn.ControlNode,
@@ -135,6 +141,7 @@ func (rn RenderableNode) Copy() RenderableNode {
func (rn RenderableNode) Prune() RenderableNode {
cp := rn.Copy()
cp.Node = report.MakeNode().WithAdjacent(cp.Node.Adjacency...)
cp.Children = nil
return cp
}
+4 -4
View File
@@ -37,7 +37,7 @@ func TestMergeRenderableNode(t *testing.T) {
Rank: "",
Pseudo: false,
Node: report.MakeNode().WithAdjacent("a1"),
Origins: report.MakeIDList("o1"),
Children: report.MakeNodeSet(report.MakeNode().WithID("child1")),
}
node2 := render.RenderableNode{
ID: "foo",
@@ -46,7 +46,7 @@ func TestMergeRenderableNode(t *testing.T) {
Rank: "rank",
Pseudo: false,
Node: report.MakeNode().WithAdjacent("a2"),
Origins: report.MakeIDList("o2"),
Children: report.MakeNodeSet(report.MakeNode().WithID("child2")),
}
want := render.RenderableNode{
ID: "foo",
@@ -54,8 +54,8 @@ func TestMergeRenderableNode(t *testing.T) {
LabelMinor: "minor",
Rank: "rank",
Pseudo: false,
Node: report.MakeNode().WithAdjacent("a1").WithAdjacent("a2"),
Origins: report.MakeIDList("o1", "o2"),
Node: report.MakeNode().WithID("foo").WithAdjacent("a1").WithAdjacent("a2"),
Children: report.MakeNodeSet(report.MakeNode().WithID("child1"), report.MakeNode().WithID("child2")),
EdgeMetadata: report.EdgeMetadata{},
}.Prune()
have := node1.Merge(node2).Prune()
+1 -6
View File
@@ -22,12 +22,7 @@ func (t TopologySelector) Stats(r report.Report) Stats {
func MakeRenderableNodes(t report.Topology) RenderableNodes {
result := RenderableNodes{}
for id, nmd := range t.Nodes {
rn := NewRenderableNode(id).WithNode(nmd)
rn.Origins = report.MakeIDList(id)
if hostNodeID, ok := nmd.Metadata[report.HostNodeID]; ok {
rn.Origins = rn.Origins.Add(hostNodeID)
}
result[id] = rn
result[id] = NewRenderableNode(id).WithNode(nmd)
}
// Push EdgeMetadata to both ends of the edges
+8 -10
View File
@@ -28,7 +28,7 @@ var (
containerID = "a1b2c3d4e5"
containerIP = "192.168.0.1"
containerName = "foo"
containerNodeID = report.MakeContainerNodeID(serverHostID, containerID)
containerNodeID = report.MakeContainerNodeID(containerID)
rpt = report.Report{
Endpoint: report.Topology{
@@ -37,13 +37,13 @@ var (
endpoint.Addr: randomIP,
endpoint.Port: randomPort,
endpoint.Conntracked: "true",
}).WithAdjacent(serverEndpointNodeID),
}).WithAdjacent(serverEndpointNodeID).WithID(randomEndpointNodeID).WithTopology("endpoint"),
serverEndpointNodeID: report.MakeNode().WithMetadata(map[string]string{
endpoint.Addr: serverIP,
endpoint.Port: serverPort,
endpoint.Conntracked: "true",
}),
}).WithID(serverEndpointNodeID).WithTopology("endpoint"),
},
},
Container: report.Topology{
@@ -55,7 +55,7 @@ var (
}).WithSets(report.Sets{
docker.ContainerIPs: report.MakeStringSet(containerIP),
docker.ContainerPorts: report.MakeStringSet(fmt.Sprintf("%s:%s->%s/tcp", serverIP, serverPort, serverPort)),
}),
}).WithID(containerNodeID).WithTopology("container"),
},
},
Host: report.Topology{
@@ -64,7 +64,7 @@ var (
report.HostNodeID: serverHostNodeID,
}).WithSets(report.Sets{
host.LocalNetworks: report.MakeStringSet("192.168.0.0/16"),
}),
}).WithID(serverHostNodeID).WithTopology("host"),
},
},
}
@@ -74,16 +74,14 @@ var (
ID: render.TheInternetID,
LabelMajor: render.TheInternetMajor,
Pseudo: true,
Node: report.MakeNode().WithAdjacent(containerID),
Origins: report.MakeIDList(randomEndpointNodeID),
Node: report.MakeNode().WithAdjacent(render.MakeContainerID(containerID)),
},
containerID: {
ID: containerID,
render.MakeContainerID(containerID): {
ID: render.MakeContainerID(containerID),
LabelMajor: containerName,
LabelMinor: serverHostID,
Rank: "",
Pseudo: false,
Origins: report.MakeIDList(containerNodeID, serverEndpointNodeID, serverHostNodeID),
Node: report.MakeNode(),
ControlNode: containerNodeID,
},
+34 -16
View File
@@ -49,7 +49,7 @@ func (r processWithContainerNameRenderer) Render(rpt report.Report) RenderableNo
if !ok {
continue
}
container, ok := containers[containerID]
container, ok := containers[MakeContainerID(containerID)]
if !ok {
continue
}
@@ -86,15 +86,10 @@ var ContainerRenderer = MakeReduce(
_, isConnected := n.Node.Metadata[IsConnected]
return inContainer || isConnected
},
Renderer: ColorConnected(Map{
Renderer: Map{
MapFunc: MapProcess2Container,
Renderer: ProcessRenderer,
}),
},
Map{
MapFunc: MapContainerIdentity,
Renderer: SelectContainer,
Renderer: ColorConnected(ProcessRenderer),
},
},
// This mapper brings in short lived connections by joining with container IPs.
@@ -114,6 +109,11 @@ var ContainerRenderer = MakeReduce(
},
),
}),
Map{
MapFunc: MapContainerIdentity,
Renderer: SelectContainer,
},
)
type containerWithImageNameRenderer struct {
@@ -135,11 +135,11 @@ func (r containerWithImageNameRenderer) Render(rpt report.Report) RenderableNode
if !ok {
continue
}
image, ok := images[imageID]
image, ok := images[MakeContainerImageID(imageID)]
if !ok {
continue
}
c.Rank = imageNameWithoutVersion(image.LabelMajor)
c.Rank = ImageNameWithoutVersion(image.LabelMajor)
c.Metadata = image.Metadata.Merge(c.Metadata)
containers[id] = c
}
@@ -191,7 +191,25 @@ var AddressRenderer = Map{
// graph from the host topology and address graph.
var HostRenderer = MakeReduce(
Map{
MapFunc: MapAddress2Host,
MapFunc: MapX2Host,
Renderer: Map{
MapFunc: MapContainerImageIdentity,
Renderer: SelectContainerImage,
},
},
Map{
MapFunc: MapX2Host,
Renderer: FilterPseudo(ContainerRenderer),
},
Map{
MapFunc: MapX2Host,
Renderer: Map{
MapFunc: MapPodIdentity,
Renderer: SelectPod,
},
},
Map{
MapFunc: MapX2Host,
Renderer: AddressRenderer,
},
Map{
@@ -205,14 +223,14 @@ var HostRenderer = MakeReduce(
var PodRenderer = Map{
MapFunc: MapCountContainers,
Renderer: MakeReduce(
Map{
MapFunc: MapPodIdentity,
Renderer: SelectPod,
},
Map{
MapFunc: MapContainer2Pod,
Renderer: ContainerRenderer,
},
Map{
MapFunc: MapPodIdentity,
Renderer: SelectPod,
},
),
}
+4 -4
View File
@@ -43,7 +43,7 @@ func TestContainerFilterRenderer(t *testing.T) {
input.Container.Nodes[fixture.ClientContainerNodeID].Metadata[docker.LabelPrefix+"works.weave.role"] = "system"
have := render.FilterSystem(render.ContainerWithImageNameRenderer).Render(input).Prune()
want := expected.RenderedContainers.Copy()
delete(want, fixture.ClientContainerID)
delete(want, expected.ClientContainerRenderedID)
if !reflect.DeepEqual(want, have) {
t.Error(test.Diff(want, have))
}
@@ -77,14 +77,14 @@ func TestPodFilterRenderer(t *testing.T) {
// tag on containers or pod namespace in the topology and ensure
// it is filtered out correctly.
input := fixture.Report.Copy()
input.Pod.Nodes[fixture.ClientPodNodeID].Metadata[kubernetes.PodID] = "kube-system/foo"
input.Pod.Nodes[fixture.ClientPodNodeID].Metadata[kubernetes.PodID] = "pod:kube-system/foo"
input.Pod.Nodes[fixture.ClientPodNodeID].Metadata[kubernetes.Namespace] = "kube-system"
input.Pod.Nodes[fixture.ClientPodNodeID].Metadata[kubernetes.PodName] = "foo"
input.Container.Nodes[fixture.ClientContainerNodeID].Metadata[docker.LabelPrefix+"io.kubernetes.pod.name"] = "kube-system/foo"
have := render.FilterSystem(render.PodRenderer).Render(input).Prune()
want := expected.RenderedPods.Copy()
delete(want, fixture.ClientPodID)
delete(want, fixture.ClientContainerID)
delete(want, expected.ClientPodRenderedID)
delete(want, expected.ClientContainerRenderedID)
if !reflect.DeepEqual(want, have) {
t.Error(test.Diff(want, have))
}