diff --git a/client/app/scripts/components/node-details.js b/client/app/scripts/components/node-details.js index 30b446d21f..bf8cd15060 100644 --- a/client/app/scripts/components/node-details.js +++ b/client/app/scripts/components/node-details.js @@ -191,11 +191,17 @@ export default class NodeDetails extends React.Component { )} - {details.docker_labels && details.docker_labels.length > 0 - &&
-
Docker Labels
- -
} + {details.tables && details.tables.length > 0 && details.tables.map(table => { + if (table.rows.length > 0) { + return ( +
+
{table.label}
+ +
+ ); + } + return null; + })} ); diff --git a/client/app/scripts/components/node-details/node-details-labels.js b/client/app/scripts/components/node-details/node-details-labels.js index 300b2a0b2b..362ff4e197 100644 --- a/client/app/scripts/components/node-details/node-details-labels.js +++ b/client/app/scripts/components/node-details/node-details-labels.js @@ -1,17 +1,47 @@ import React from 'react'; -export default function NodeDetailsLabels({rows}) { - return ( -
- {rows.map(field => (
-
- {field.label} -
-
- {field.value} +import ShowMore from '../show-more'; + +export default class NodeDetailsLabels extends React.Component { + + constructor(props, context) { + super(props, context); + this.DEFAULT_LIMIT = 5; + this.state = { + limit: this.DEFAULT_LIMIT, + }; + this.handleLimitClick = this.handleLimitClick.bind(this); + } + + handleLimitClick() { + const limit = this.state.limit ? 0 : this.DEFAULT_LIMIT; + this.setState({limit}); + } + + render() { + let rows = this.props.rows; + const limited = rows && this.state.limit > 0 && rows.length > this.state.limit; + const expanded = this.state.limit === 0; + const notShown = rows.length - this.DEFAULT_LIMIT; + if (rows && limited) { + rows = rows.slice(0, this.state.limit); + } + + return ( +
+ {rows.map(field => (
+
+ {field.label} +
+
+ {field.value} +
-
- ))} -
- ); + ))} + +
+ ); + } } diff --git a/probe/docker/container.go b/probe/docker/container.go index 46e6849db7..61ce977191 100644 --- a/probe/docker/container.go +++ b/probe/docker/container.go @@ -65,6 +65,9 @@ const ( NetworkModeHost = "host" + LabelPrefix = "docker_label_" + EnvPrefix = "docker_env_" + stopTimeout = 10 ) @@ -323,6 +326,18 @@ func (c *container) metrics() report.Metrics { return result } +func (c *container) env() map[string]string { + result := map[string]string{} + for _, value := range c.container.Config.Env { + v := strings.SplitN(value, "=", 2) + if len(v) != 2 { + continue + } + result[v[0]] = v[1] + } + return result +} + func (c *container) GetNode(hostID string, localAddrs []net.IP) report.Node { c.RLock() defer c.RUnlock() @@ -376,8 +391,8 @@ func (c *container) GetNode(hostID string, localAddrs []net.IP) report.Node { result = result.WithControls(StartContainer) } - result = AddLabels(result, c.container.Config.Labels) - result = AddEnv(result, c.container.Config.Env) + result = result.AddTable(LabelPrefix, c.container.Config.Labels) + result = result.AddTable(EnvPrefix, c.env()) result = result.WithMetrics(c.metrics()) return result } diff --git a/probe/docker/env.go b/probe/docker/env.go deleted file mode 100644 index 1d216750b4..0000000000 --- a/probe/docker/env.go +++ /dev/null @@ -1,39 +0,0 @@ -package docker - -import ( - "strings" - - "github.com/weaveworks/scope/report" -) - -// EnvPrefix is the key prefix used for Docker environment variables in Node -// (e.g. "TERM=vt200" will get encoded as "docker_env_TERM"="vt200" in the -// metadata) -const EnvPrefix = "docker_env_" - -// AddEnv appends Docker environment variables to the Node from a topology. -func AddEnv(node report.Node, env []string) report.Node { - node = node.Copy() - for _, value := range env { - v := strings.SplitN(value, "=", 2) - if len(v) == 2 { - key, value := v[0], v[1] - node = node.WithLatests(map[string]string{ - EnvPrefix + key: value, - }) - } - } - return node -} - -// ExtractEnv returns the list of Docker environment variables given a Node from a topology. -func ExtractEnv(node report.Node) map[string]string { - result := map[string]string{} - node.Latest.ForEach(func(key, value string) { - if strings.HasPrefix(key, EnvPrefix) { - env := key[len(EnvPrefix):] - result[env] = value - } - }) - return result -} diff --git a/probe/docker/env_test.go b/probe/docker/env_test.go deleted file mode 100644 index 64c68dad61..0000000000 --- a/probe/docker/env_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package docker_test - -import ( - "testing" - - "github.com/weaveworks/scope/probe/docker" - "github.com/weaveworks/scope/report" - _ "github.com/weaveworks/scope/test" -) - -func TestEnv(t *testing.T) { - given := []string{ - "TERM=vt200", - "SHELL=/bin/ksh", - "FOO1=\"foo=bar\"", - "FOO2", - } - nmd := report.MakeNode("foo") - - nmd = docker.AddEnv(nmd, given) - have := docker.ExtractEnv(nmd) - - if "vt200" != have["TERM"] { - t.Errorf("Expected \"vt200\", got \"%s\"", have["TERM"]) - } - - if "/bin/ksh" != have["SHELL"] { - t.Errorf("Expected \"/bin/ksh\", got \"%s\"", have["SHELL"]) - } - - if "\"foo=bar\"" != have["FOO1"] { - t.Errorf("Expected \"\"foo=bar\"\", got \"%s\"", have["FOO1"]) - } - - if len(have) != 3 { - t.Errorf("Expected only 3 items, got %d", len(have)) - } -} diff --git a/probe/docker/labels.go b/probe/docker/labels.go deleted file mode 100644 index 3fd7899646..0000000000 --- a/probe/docker/labels.go +++ /dev/null @@ -1,35 +0,0 @@ -package docker - -import ( - "strings" - - "github.com/weaveworks/scope/report" -) - -// LabelPrefix is the key prefix used for Docker labels in Node (e.g. a -// Docker label "labelKey"="labelValue" will get encoded as -// "docker_label_labelKey"="dockerValue" in the metadata) -const LabelPrefix = "docker_label_" - -// AddLabels appends Docker labels to the Node from a topology. -func AddLabels(node report.Node, labels map[string]string) report.Node { - node = node.Copy() - for key, value := range labels { - node = node.WithLatests(map[string]string{ - LabelPrefix + key: value, - }) - } - return node -} - -// ExtractLabels returns the list of Docker labels given a Node from a topology. -func ExtractLabels(node report.Node) map[string]string { - result := map[string]string{} - node.Latest.ForEach(func(key, value string) { - if strings.HasPrefix(key, LabelPrefix) { - label := key[len(LabelPrefix):] - result[label] = value - } - }) - return result -} diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go index facbb6673b..0b4ea8ff29 100644 --- a/probe/docker/reporter.go +++ b/probe/docker/reporter.go @@ -40,6 +40,15 @@ var ( ImageID: {ID: ImageID, Label: "Image ID", From: report.FromLatest, Truncate: 12, Priority: 1}, report.Container: {ID: report.Container, Label: "# Containers", From: report.FromCounters, Datatype: "number", Priority: 2}, } + + ContainerTableTemplates = report.TableTemplates{ + LabelPrefix: {ID: LabelPrefix, Label: "Docker Labels", Prefix: LabelPrefix}, + EnvPrefix: {ID: EnvPrefix, Label: "Environment Variables", Prefix: EnvPrefix}, + } + + ContainerImageTableTemplates = report.TableTemplates{ + LabelPrefix: {ID: LabelPrefix, Label: "Docker Labels", Prefix: LabelPrefix}, + } ) // Reporter generate Reports containing Container and ContainerImage topologies @@ -96,7 +105,8 @@ func (r *Reporter) Report() (report.Report, error) { func (r *Reporter) containerTopology(localAddrs []net.IP) report.Topology { result := report.MakeTopology(). WithMetadataTemplates(ContainerMetadataTemplates). - WithMetricTemplates(ContainerMetricTemplates) + WithMetricTemplates(ContainerMetricTemplates). + WithTableTemplates(ContainerTableTemplates) result.Controls.AddControl(report.Control{ ID: StopContainer, Human: "Stop", @@ -143,7 +153,9 @@ func (r *Reporter) containerTopology(localAddrs []net.IP) report.Topology { } func (r *Reporter) containerImageTopology() report.Topology { - result := report.MakeTopology().WithMetadataTemplates(ContainerImageMetadataTemplates) + result := report.MakeTopology(). + WithMetadataTemplates(ContainerImageMetadataTemplates). + WithTableTemplates(ContainerImageTableTemplates) r.registry.WalkImages(func(image *docker_client.APIImages) { imageID := trimImageID(image.ID) @@ -151,7 +163,7 @@ func (r *Reporter) containerImageTopology() report.Topology { node := report.MakeNodeWith(nodeID, map[string]string{ ImageID: imageID, }) - node = AddLabels(node, image.Labels) + node = node.AddTable(LabelPrefix, image.Labels) if len(image.RepoTags) > 0 { node = node.WithLatests(map[string]string{ImageName: image.RepoTags[0]}) diff --git a/probe/kubernetes/pod.go b/probe/kubernetes/pod.go index 81a98f0419..0349ea87ce 100644 --- a/probe/kubernetes/pod.go +++ b/probe/kubernetes/pod.go @@ -16,6 +16,7 @@ const ( PodCreated = "kubernetes_pod_created" PodContainerIDs = "kubernetes_pod_container_ids" PodState = "kubernetes_pod_state" + PodLabelPrefix = "kubernetes_pod_labels_" ServiceIDs = "kubernetes_service_ids" ) @@ -99,5 +100,5 @@ func (p *pod) GetNode() report.Node { Add(report.Service, report.MakeStringSet(report.MakeServiceNodeID(p.Namespace(), segments[1]))), ) } - return n + return n.AddTable(PodLabelPrefix, p.ObjectMeta.Labels) } diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go index c195e2421f..56e7293e1b 100644 --- a/probe/kubernetes/reporter.go +++ b/probe/kubernetes/reporter.go @@ -22,6 +22,14 @@ var ( ServiceCreated: {ID: ServiceCreated, Label: "Created", From: report.FromLatest, Priority: 3}, ServiceIP: {ID: ServiceIP, Label: "Internal IP", From: report.FromLatest, Priority: 4}, } + + PodTableTemplates = report.TableTemplates{ + PodLabelPrefix: {ID: PodLabelPrefix, Label: "Kubernetes Labels", Prefix: PodLabelPrefix}, + } + + ServiceTableTemplates = report.TableTemplates{ + ServiceLabelPrefix: {ID: ServiceLabelPrefix, Label: "Kubernetes Labels", Prefix: ServiceLabelPrefix}, + } ) // Reporter generate Reports containing Container and ContainerImage topologies @@ -58,7 +66,9 @@ func (r *Reporter) Report() (report.Report, error) { func (r *Reporter) serviceTopology() (report.Topology, []Service, error) { var ( - result = report.MakeTopology().WithMetadataTemplates(ServiceMetadataTemplates) + result = report.MakeTopology(). + WithMetadataTemplates(ServiceMetadataTemplates). + WithTableTemplates(ServiceTableTemplates) services = []Service{} ) err := r.client.WalkServices(func(s Service) error { @@ -71,7 +81,9 @@ func (r *Reporter) serviceTopology() (report.Topology, []Service, error) { func (r *Reporter) podTopology(services []Service) (report.Topology, report.Topology, error) { var ( - pods = report.MakeTopology().WithMetadataTemplates(PodMetadataTemplates) + pods = report.MakeTopology(). + WithMetadataTemplates(PodMetadataTemplates). + WithTableTemplates(PodTableTemplates) containers = report.MakeTopology() selectors = map[string]labels.Selector{} ) diff --git a/probe/kubernetes/service.go b/probe/kubernetes/service.go index c1a73798b2..37460adf56 100644 --- a/probe/kubernetes/service.go +++ b/probe/kubernetes/service.go @@ -10,10 +10,11 @@ import ( // These constants are keys used in node metadata const ( - ServiceID = "kubernetes_service_id" - ServiceName = "kubernetes_service_name" - ServiceCreated = "kubernetes_service_created" - ServiceIP = "kubernetes_service_ip" + ServiceID = "kubernetes_service_id" + ServiceName = "kubernetes_service_name" + ServiceCreated = "kubernetes_service_created" + ServiceIP = "kubernetes_service_ip" + ServiceLabelPrefix = "kubernetes_service_label_" ) // Service represents a Kubernetes service @@ -60,5 +61,5 @@ func (s *service) GetNode() report.Node { ServiceCreated: s.ObjectMeta.CreationTimestamp.Format(time.RFC822), Namespace: s.Namespace(), ServiceIP: s.Spec.ClusterIP, - }) + }).AddTable(ServiceLabelPrefix, s.Labels) } diff --git a/render/detailed/docker_labels.go b/render/detailed/docker_labels.go deleted file mode 100644 index 0084475d86..0000000000 --- a/render/detailed/docker_labels.go +++ /dev/null @@ -1,39 +0,0 @@ -package detailed - -import ( - "sort" - - "github.com/weaveworks/scope/probe/docker" - "github.com/weaveworks/scope/report" -) - -// NodeDockerLabels 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 NodeDockerLabels(nmd report.Node) []report.MetadataRow { - if _, ok := nmd.Counters.Lookup(nmd.Topology); ok { - // This is a group of nodes, so no docker labels! - return nil - } - - if nmd.Topology != report.Container && nmd.Topology != report.ContainerImage { - return nil - } - - var rows []report.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, report.MetadataRow{ - ID: "label_" + labelKey, - Label: labelKey, - Value: labels[labelKey], - }) - } - return rows -} diff --git a/render/detailed/node_test.go b/render/detailed/node_test.go index eec2c22fae..39a9b489cf 100644 --- a/render/detailed/node_test.go +++ b/render/detailed/node_test.go @@ -186,12 +186,6 @@ func TestMakeDetailedContainerNode(t *testing.T) { {ID: "docker_container_state_human", Label: "State", Value: "running", Priority: 2}, {ID: "docker_image_id", Label: "Image ID", Value: fixture.ServerContainerImageID, Priority: 11}, }, - DockerLabels: []report.MetadataRow{ - {ID: "label_" + detailed.AmazonECSContainerNameLabel, Label: detailed.AmazonECSContainerNameLabel, Value: `server`}, - {ID: "label_foo1", Label: "foo1", Value: `bar1`}, - {ID: "label_foo2", Label: "foo2", Value: `bar2`}, - {ID: "label_io.kubernetes.pod.name", Label: "io.kubernetes.pod.name", Value: "ping/pong-b"}, - }, Metrics: []report.MetricRow{ { ID: docker.CPUTotalUsage, diff --git a/render/detailed/summary.go b/render/detailed/summary.go index 4bc9c38a86..b657112b4f 100644 --- a/render/detailed/summary.go +++ b/render/detailed/summary.go @@ -61,18 +61,18 @@ type Column struct { // NodeSummary is summary information about a child for a Node. type NodeSummary struct { - ID string `json:"id"` - Label string `json:"label"` - LabelMinor string `json:"label_minor"` - Rank string `json:"rank"` - Shape string `json:"shape,omitempty"` - Stack bool `json:"stack,omitempty"` - Linkable bool `json:"linkable,omitempty"` // Whether this node can be linked-to - Pseudo bool `json:"pseudo,omitempty"` - Metadata []report.MetadataRow `json:"metadata,omitempty"` - DockerLabels []report.MetadataRow `json:"docker_labels,omitempty"` - Metrics []report.MetricRow `json:"metrics,omitempty"` - Adjacency report.IDList `json:"adjacency,omitempty"` + ID string `json:"id"` + Label string `json:"label"` + LabelMinor string `json:"label_minor"` + Rank string `json:"rank"` + Shape string `json:"shape,omitempty"` + Stack bool `json:"stack,omitempty"` + Linkable bool `json:"linkable,omitempty"` // Whether this node can be linked-to + Pseudo bool `json:"pseudo,omitempty"` + Metadata []report.MetadataRow `json:"metadata,omitempty"` + Metrics []report.MetricRow `json:"metrics,omitempty"` + Tables []report.Table `json:"tables,omitempty"` + Adjacency report.IDList `json:"adjacency,omitempty"` } // MakeNodeSummary summarizes a node, if possible. @@ -117,8 +117,8 @@ func (n NodeSummary) Copy() NodeSummary { for _, row := range n.Metadata { result.Metadata = append(result.Metadata, row.Copy()) } - for _, row := range n.DockerLabels { - result.DockerLabels = append(result.DockerLabels, row.Copy()) + for _, table := range n.Tables { + result.Tables = append(result.Tables, table.Copy()) } for _, row := range n.Metrics { result.Metrics = append(result.Metrics, row.Copy()) @@ -128,13 +128,13 @@ func (n NodeSummary) Copy() NodeSummary { func baseNodeSummary(r report.Report, n report.Node) NodeSummary { return NodeSummary{ - ID: n.ID, - Shape: Circle, - Linkable: true, - Metadata: NodeMetadata(r, n), - DockerLabels: NodeDockerLabels(n), - Metrics: NodeMetrics(r, n), - Adjacency: n.Adjacency.Copy(), + ID: n.ID, + Shape: Circle, + Linkable: true, + Metadata: NodeMetadata(r, n), + Metrics: NodeMetrics(r, n), + Tables: NodeTables(r, n), + Adjacency: n.Adjacency.Copy(), } } diff --git a/render/detailed/tables.go b/render/detailed/tables.go new file mode 100644 index 0000000000..63fe918784 --- /dev/null +++ b/render/detailed/tables.go @@ -0,0 +1,20 @@ +package detailed + +import ( + "github.com/weaveworks/scope/report" +) + +// NodeTables produces a list of tables (to be consumed directly by the UI) based +// on the report and the node. It uses the report to get the templates for the node's +// topology. +func NodeTables(r report.Report, n report.Node) []report.Table { + if _, ok := n.Counters.Lookup(n.Topology); ok { + // This is a group of nodes, so no tables! + return nil + } + + if topology, ok := r.Topology(n.Topology); ok { + return topology.TableTemplates.Tables(n) + } + return nil +} diff --git a/render/detailed/docker_labels_test.go b/render/detailed/tables_test.go similarity index 64% rename from render/detailed/docker_labels_test.go rename to render/detailed/tables_test.go index 8b8049f086..ebdf3b2ee8 100644 --- a/render/detailed/docker_labels_test.go +++ b/render/detailed/tables_test.go @@ -11,14 +11,19 @@ import ( "github.com/weaveworks/scope/test/fixture" ) -func TestNodeDockerLabels(t *testing.T) { +func TestNodeTables(t *testing.T) { inputs := []struct { name string + rpt report.Report node report.Node - want []report.MetadataRow + want []report.Table }{ { name: "container", + rpt: report.Report{ + Container: report.MakeTopology(). + WithTableTemplates(docker.ContainerTableTemplates), + }, node: report.MakeNodeWith(fixture.ClientContainerNodeID, map[string]string{ docker.ContainerID: fixture.ClientContainerID, docker.LabelPrefix + "label1": "label1value", @@ -26,16 +31,28 @@ func TestNodeDockerLabels(t *testing.T) { }).WithTopology(report.Container).WithSets(report.EmptySets. Add(docker.ContainerIPs, report.MakeStringSet("10.10.10.0/24", "10.10.10.1/24")), ), - want: []report.MetadataRow{ + want: []report.Table{ + { + ID: docker.EnvPrefix, + Label: "Environment Variables", + Rows: []report.MetadataRow{}, + }, { - ID: "label_label1", - Label: "label1", - Value: "label1value", + ID: docker.LabelPrefix, + Label: "Docker Labels", + Rows: []report.MetadataRow{ + { + ID: "label_label1", + Label: "label1", + Value: "label1value", + }, + }, }, }, }, { name: "unknown topology", + rpt: report.MakeReport(), node: report.MakeNodeWith(fixture.ClientContainerNodeID, map[string]string{ docker.ContainerID: fixture.ClientContainerID, }).WithTopology("foobar"), @@ -43,7 +60,7 @@ func TestNodeDockerLabels(t *testing.T) { }, } for _, input := range inputs { - have := detailed.NodeDockerLabels(input.node) + have := detailed.NodeTables(input.rpt, input.node) if !reflect.DeepEqual(input.want, have) { t.Errorf("%s: %s", input.name, test.Diff(input.want, have)) } diff --git a/report/table.go b/report/table.go new file mode 100644 index 0000000000..806a1b23c9 --- /dev/null +++ b/report/table.go @@ -0,0 +1,146 @@ +package report + +import ( + "sort" + "strings" + + "github.com/weaveworks/scope/common/mtime" +) + +// AddTable appends arbirary key-value pairs to the Node, returning a new node. +func (node Node) AddTable(prefix string, labels map[string]string) Node { + for key, value := range labels { + node = node.WithLatest(prefix+key, mtime.Now(), value) + } + return node +} + +// ExtractTable returns the key-value pairs with the given prefix from this Node, +func (node Node) ExtractTable(prefix string) map[string]string { + result := map[string]string{} + node.Latest.ForEach(func(key, value string) { + if strings.HasPrefix(key, prefix) { + label := key[len(prefix):] + result[label] = value + } + }) + return result +} + +// Table is the type for a table in the UI. +type Table struct { + ID string `json:"id"` + Label string `json:"label"` + Rows []MetadataRow `json:"rows"` +} + +type tablesByID []Table + +func (t tablesByID) Len() int { return len(t) } +func (t tablesByID) Swap(i, j int) { t[i], t[j] = t[j], t[i] } +func (t tablesByID) Less(i, j int) bool { return t[i].ID < t[j].ID } + +// Copy returns a copy of the Table. +func (t Table) Copy() Table { + result := Table{ + ID: t.ID, + Label: t.Label, + Rows: make([]MetadataRow, 0, len(t.Rows)), + } + for _, row := range t.Rows { + result.Rows = append(result.Rows, row) + } + return result +} + +// TableTemplate describes how to render a table for the UI. +type TableTemplate struct { + ID string `json:"id"` + Label string `json:"label"` + Prefix string `json:"prefix"` +} + +// Copy returns a copy of the TableTemplate +func (t TableTemplate) Copy() TableTemplate { + return TableTemplate{ + ID: t.ID, + Label: t.Label, + Prefix: t.Prefix, + } +} + +// Merge other into t, returning a fresh copy. Does fieldwise max - +// whilst this isn't particularly meaningful, at least it idempotent, +// commutativite and associative. +func (t TableTemplate) Merge(other TableTemplate) TableTemplate { + max := func(s1, s2 string) string { + if s1 > s2 { + return s1 + } + return s2 + } + + return TableTemplate{ + ID: max(t.ID, other.ID), + Label: max(t.Label, other.Label), + Prefix: max(t.Prefix, other.Prefix), + } +} + +// TableTemplates is a mergeable set of TableTemplate +type TableTemplates map[string]TableTemplate + +// Tables renders the TableTemplates for a given node. +func (t TableTemplates) Tables(node Node) []Table { + var result []Table + for _, template := range t { + table := Table{ + ID: template.ID, + Label: template.Label, + Rows: []MetadataRow{}, + } + rows := node.ExtractTable(template.Prefix) + keys := make([]string, 0, len(rows)) + for k := range rows { + keys = append(keys, k) + } + sort.Strings(keys) + for _, key := range keys { + table.Rows = append(table.Rows, MetadataRow{ + ID: "label_" + key, + Label: key, + Value: rows[key], + }) + } + result = append(result, table) + } + sort.Sort(tablesByID(result)) + return result +} + +// Copy returns a value copy of the TableTemplates +func (t TableTemplates) Copy() TableTemplates { + if t == nil { + return nil + } + result := TableTemplates{} + for k, v := range t { + result[k] = v.Copy() + } + return result +} + +// Merge merges two sets of TableTemplates +func (t TableTemplates) Merge(other TableTemplates) TableTemplates { + result := t.Copy() + for k, v := range other { + if result == nil { + result = TableTemplates{} + } + if existing, ok := result[k]; ok { + v = v.Merge(existing) + } + result[k] = v + } + return result +} diff --git a/probe/docker/labels_test.go b/report/table_test.go similarity index 63% rename from probe/docker/labels_test.go rename to report/table_test.go index 6db8fdfc0f..b1866b072a 100644 --- a/probe/docker/labels_test.go +++ b/report/table_test.go @@ -1,23 +1,22 @@ -package docker_test +package report_test import ( "reflect" "testing" - "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/report" "github.com/weaveworks/scope/test" ) -func TestLabels(t *testing.T) { +func TestTables(t *testing.T) { want := map[string]string{ "foo1": "bar1", "foo2": "bar2", } nmd := report.MakeNode("foo1") - nmd = docker.AddLabels(nmd, want) - have := docker.ExtractLabels(nmd) + nmd = nmd.AddTable("foo_", want) + have := nmd.ExtractTable("foo_") if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) diff --git a/report/topology.go b/report/topology.go index 542573423b..da783e3b7c 100644 --- a/report/topology.go +++ b/report/topology.go @@ -14,6 +14,7 @@ type Topology struct { Controls `json:"controls,omitempty"` MetadataTemplates `json:"metadata_templates,omitempty"` MetricTemplates `json:"metric_templates,omitempty"` + TableTemplates `json:"table_templates,omitempty"` } // MakeTopology gives you a Topology. @@ -32,6 +33,7 @@ func (t Topology) WithMetadataTemplates(other MetadataTemplates) Topology { Controls: t.Controls.Copy(), MetadataTemplates: t.MetadataTemplates.Merge(other), MetricTemplates: t.MetricTemplates.Copy(), + TableTemplates: t.TableTemplates.Copy(), } } @@ -43,6 +45,19 @@ func (t Topology) WithMetricTemplates(other MetricTemplates) Topology { Controls: t.Controls.Copy(), MetadataTemplates: t.MetadataTemplates.Copy(), MetricTemplates: t.MetricTemplates.Merge(other), + TableTemplates: t.TableTemplates.Copy(), + } +} + +// WithTableTemplates merges some table templates into this topology, +// returning a new topology. +func (t Topology) WithTableTemplates(other TableTemplates) Topology { + return Topology{ + Nodes: t.Nodes.Copy(), + Controls: t.Controls.Copy(), + MetadataTemplates: t.MetadataTemplates.Copy(), + MetricTemplates: t.MetricTemplates.Copy(), + TableTemplates: t.TableTemplates.Merge(other), } } @@ -66,6 +81,7 @@ func (t Topology) Copy() Topology { Controls: t.Controls.Copy(), MetadataTemplates: t.MetadataTemplates.Copy(), MetricTemplates: t.MetricTemplates.Copy(), + TableTemplates: t.TableTemplates.Copy(), } } @@ -77,6 +93,7 @@ func (t Topology) Merge(other Topology) Topology { Controls: t.Controls.Merge(other.Controls), MetadataTemplates: t.MetadataTemplates.Merge(other.MetadataTemplates), MetricTemplates: t.MetricTemplates.Merge(other.MetricTemplates), + TableTemplates: t.TableTemplates.Merge(other.TableTemplates), } }