diff --git a/client/app/scripts/components/node-details.js b/client/app/scripts/components/node-details.js index b8abffd1b7..63a4d9b8a9 100644 --- a/client/app/scripts/components/node-details.js +++ b/client/app/scripts/components/node-details.js @@ -203,20 +203,22 @@ class NodeDetails extends React.Component { ))} - {details.tables && details.tables.length > 0 && details.tables.map((table) => { - if (table.rows.length > 0) { + {details.propertyLists && details.propertyLists.map((propertyList) => { + if (propertyList.rows.length > 0) { return ( -
+
- {table.label} - {table.truncationCount > 0 && 0 && - + }
+ rows={propertyList.rows} + controls={propertyList.controls} + matches={nodeMatches.get('propertyLists')} + />
); } diff --git a/client/app/scripts/utils/__tests__/search-utils-test.js b/client/app/scripts/utils/__tests__/search-utils-test.js index 301b431a2c..356562ebe7 100644 --- a/client/app/scripts/utils/__tests__/search-utils-test.js +++ b/client/app/scripts/utils/__tests__/search-utils-test.js @@ -17,7 +17,15 @@ describe('SearchUtils', () => { id: 'metric1', label: 'Metric 1', value: 1 - }] + }], + propertyLists: [{ + id: 'labels1', + rows: [{ + id: 'label1', + label: 'Label 1', + value: 'Value 1' + }] + }], }, n2: { id: 'n2', @@ -28,7 +36,7 @@ describe('SearchUtils', () => { value: 'value 2' }], tables: [{ - id: 'metric1', + id: 'table1', rows: [{ id: 'row1', label: 'Row 1', @@ -242,6 +250,14 @@ describe('SearchUtils', () => { expect(matches.size).toEqual(1); expect(matches.getIn(['n2', 'tables', 'row1']).text).toBe('Row Value 1'); }); + + it('should match on a property lists field', () => { + const nodes = nodeSets.someNodes; + const matches = fun(nodes, {query: 'Value 1'}); + expect(matches.size).toEqual(2); + expect(matches.getIn(['n2', 'tables', 'row1']).text).toBe('Row Value 1'); + expect(matches.getIn(['n1', 'propertyLists', 'label1']).text).toBe('Value 1'); + }); }); describe('updateNodeMatches', () => { diff --git a/client/app/scripts/utils/search-utils.js b/client/app/scripts/utils/search-utils.js index b8f74d0471..70299b1707 100644 --- a/client/app/scripts/utils/search-utils.js +++ b/client/app/scripts/utils/search-utils.js @@ -148,7 +148,7 @@ export function searchTopology(nodes, { prefix, query, metric, comp, value }) { }); } - // tables (envvars and labels) + // node tables const tables = node.get('tables'); if (tables) { tables.forEach((table) => { @@ -161,6 +161,20 @@ export function searchTopology(nodes, { prefix, query, metric, comp, value }) { } }); } + + // property lists (envvars and labels) + const propertyLists = node.get('propertyLists'); + if (propertyLists) { + propertyLists.forEach((propertyList) => { + if (propertyList.get('rows')) { + propertyList.get('rows').forEach((field) => { + const keyPath = [nodeId, 'propertyLists', field.get('id')]; + nodeMatches = findNodeMatch(nodeMatches, keyPath, field.get('value'), + query, prefix, field.get('label')); + }); + } + }); + } } else if (metric) { const metrics = node.get('metrics'); if (metrics) { @@ -267,12 +281,18 @@ export function getSearchableFields(nodes) { )) ), makeSet()); + const propertyLabels = nodes.reduce((labels, node) => ( + labels.union(get(node, 'propertyLists').flatMap(t => (t.get('rows') || makeList) + .map(f => f.get('label')) + )) + ), makeSet()); + const metricLabels = nodes.reduce((labels, node) => ( labels.union(get(node, 'metrics').map(f => f.get('label'))) ), makeSet()); return makeMap({ - fields: baseLabels.union(metadataLabels, parentLabels, tableRowLabels) + fields: baseLabels.union(metadataLabels, parentLabels, tableRowLabels, propertyLabels) .map(slugify) .toList() .sort(), diff --git a/probe/docker/container.go b/probe/docker/container.go index 50856ee0fa..419fb86059 100644 --- a/probe/docker/container.go +++ b/probe/docker/container.go @@ -379,8 +379,8 @@ func (c *container) getBaseNode() report.Node { }).WithParents(report.EmptySets. Add(report.ContainerImage, report.MakeStringSet(report.MakeContainerImageNodeID(c.Image()))), ) - result = result.AddPrefixTable(LabelPrefix, c.container.Config.Labels) - result = result.AddPrefixTable(EnvPrefix, c.env()) + result = result.AddPrefixPropertyList(LabelPrefix, c.container.Config.Labels) + result = result.AddPrefixPropertyList(EnvPrefix, c.env()) return result } diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go index b1e2eb75c8..3d64df6cf1 100644 --- a/probe/docker/reporter.go +++ b/probe/docker/reporter.go @@ -47,9 +47,9 @@ var ( report.Container: {ID: report.Container, Label: "# Containers", From: report.FromCounters, Datatype: "number", Priority: 2}, } - ContainerTableTemplates = report.TableTemplates{ + ContainerPropertyListTemplates = report.PropertyListTemplates{ ImageTableID: {ID: ImageTableID, Label: "Image", - FixedRows: map[string]string{ + FixedProperties: map[string]string{ ImageID: "ID", ImageName: "Name", ImageSize: "Size", @@ -60,7 +60,7 @@ var ( EnvPrefix: {ID: EnvPrefix, Label: "Environment Variables", Prefix: EnvPrefix}, } - ContainerImageTableTemplates = report.TableTemplates{ + ContainerImagePropertyListTemplates = report.PropertyListTemplates{ ImageLabelPrefix: {ID: ImageLabelPrefix, Label: "Docker Labels", Prefix: ImageLabelPrefix}, } @@ -181,7 +181,7 @@ func (r *Reporter) containerTopology(localAddrs []net.IP) report.Topology { result := report.MakeTopology(). WithMetadataTemplates(ContainerMetadataTemplates). WithMetricTemplates(ContainerMetricTemplates). - WithTableTemplates(ContainerTableTemplates) + WithPropertyListTemplates(ContainerPropertyListTemplates) result.Controls.AddControls(ContainerControls) metadata := map[string]string{report.ControlProbeID: r.probeID} @@ -244,7 +244,7 @@ func (r *Reporter) containerTopology(localAddrs []net.IP) report.Topology { func (r *Reporter) containerImageTopology() report.Topology { result := report.MakeTopology(). WithMetadataTemplates(ContainerImageMetadataTemplates). - WithTableTemplates(ContainerImageTableTemplates) + WithPropertyListTemplates(ContainerImagePropertyListTemplates) r.registry.WalkImages(func(image docker_client.APIImages) { imageID := trimImageID(image.ID) @@ -258,7 +258,7 @@ func (r *Reporter) containerImageTopology() report.Topology { } nodeID := report.MakeContainerImageNodeID(imageID) node := report.MakeNodeWith(nodeID, latests) - node = node.AddPrefixTable(ImageLabelPrefix, image.Labels) + node = node.AddPrefixPropertyList(ImageLabelPrefix, image.Labels) result.AddNode(node) }) diff --git a/probe/kubernetes/meta.go b/probe/kubernetes/meta.go index c7db23d0c7..fc8188ca3d 100644 --- a/probe/kubernetes/meta.go +++ b/probe/kubernetes/meta.go @@ -56,5 +56,5 @@ func (m meta) MetaNode(id string) report.Node { Name: m.Name(), Namespace: m.Namespace(), Created: m.Created(), - }).AddPrefixTable(LabelPrefix, m.Labels()) + }).AddPrefixPropertyList(LabelPrefix, m.Labels()) } diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go index 44c7c564a3..ef8360cd3e 100644 --- a/probe/kubernetes/reporter.go +++ b/probe/kubernetes/reporter.go @@ -67,7 +67,7 @@ var ( ReplicaSetMetricTemplates = PodMetricTemplates - TableTemplates = report.TableTemplates{ + PropertyListTemplates = report.PropertyListTemplates{ LabelPrefix: {ID: LabelPrefix, Label: "Kubernetes Labels", Prefix: LabelPrefix}, } @@ -223,7 +223,7 @@ func (r *Reporter) serviceTopology() (report.Topology, []Service, error) { result = report.MakeTopology(). WithMetadataTemplates(ServiceMetadataTemplates). WithMetricTemplates(ServiceMetricTemplates). - WithTableTemplates(TableTemplates) + WithPropertyListTemplates(PropertyListTemplates) services = []Service{} ) err := r.client.WalkServices(func(s Service) error { @@ -258,7 +258,7 @@ func (r *Reporter) deploymentTopology(probeID string) (report.Topology, []Deploy result = report.MakeTopology(). WithMetadataTemplates(DeploymentMetadataTemplates). WithMetricTemplates(DeploymentMetricTemplates). - WithTableTemplates(TableTemplates) + WithPropertyListTemplates(PropertyListTemplates) deployments = []Deployment{} ) result.Controls.AddControls(ScalingControls) @@ -276,7 +276,7 @@ func (r *Reporter) replicaSetTopology(probeID string, deployments []Deployment) result = report.MakeTopology(). WithMetadataTemplates(ReplicaSetMetadataTemplates). WithMetricTemplates(ReplicaSetMetricTemplates). - WithTableTemplates(TableTemplates) + WithPropertyListTemplates(PropertyListTemplates) replicaSets = []ReplicaSet{} selectors = []func(labelledChild){} ) @@ -355,7 +355,7 @@ func (r *Reporter) podTopology(services []Service, replicaSets []ReplicaSet) (re pods = report.MakeTopology(). WithMetadataTemplates(PodMetadataTemplates). WithMetricTemplates(PodMetricTemplates). - WithTableTemplates(TableTemplates) + WithPropertyListTemplates(PropertyListTemplates) selectors = []func(labelledChild){} ) pods.Controls.AddControl(report.Control{ diff --git a/probe/overlay/weave.go b/probe/overlay/weave.go index 819d88eb5a..1087156395 100644 --- a/probe/overlay/weave.go +++ b/probe/overlay/weave.go @@ -19,34 +19,34 @@ import ( // Keys for use in Node const ( - WeavePeerName = "weave_peer_name" - WeavePeerNickName = "weave_peer_nick_name" - WeaveDNSHostname = "weave_dns_hostname" - WeaveMACAddress = "weave_mac_address" - WeaveVersion = "weave_version" - WeaveEncryption = "weave_encryption" - WeaveProtocol = "weave_protocol" - WeavePeerDiscovery = "weave_peer_discovery" - WeaveTargetCount = "weave_target_count" - WeaveConnectionCount = "weave_connection_count" - WeavePeerCount = "weave_peer_count" - WeaveTrustedSubnets = "weave_trusted_subnet_count" - WeaveIPAMTableID = "weave_ipam_table" - WeaveIPAMStatus = "weave_ipam_status" - WeaveIPAMRange = "weave_ipam_range" - WeaveIPAMDefaultSubnet = "weave_ipam_default_subnet" - WeaveDNSTableID = "weave_dns_table" - WeaveDNSDomain = "weave_dns_domain" - WeaveDNSUpstream = "weave_dns_upstream" - WeaveDNSTTL = "weave_dns_ttl" - WeaveDNSEntryCount = "weave_dns_entry_count" - WeaveProxyTableID = "weave_proxy_table" - WeaveProxyStatus = "weave_proxy_status" - WeaveProxyAddress = "weave_proxy_address" - WeavePluginTableID = "weave_plugin_table" - WeavePluginStatus = "weave_plugin_status" - WeavePluginDriver = "weave_plugin_driver" - WeaveConnectionsTablePrefix = "weave_connections_table_" + WeavePeerName = "weave_peer_name" + WeavePeerNickName = "weave_peer_nick_name" + WeaveDNSHostname = "weave_dns_hostname" + WeaveMACAddress = "weave_mac_address" + WeaveVersion = "weave_version" + WeaveEncryption = "weave_encryption" + WeaveProtocol = "weave_protocol" + WeavePeerDiscovery = "weave_peer_discovery" + WeaveTargetCount = "weave_target_count" + WeaveConnectionCount = "weave_connection_count" + WeavePeerCount = "weave_peer_count" + WeaveTrustedSubnets = "weave_trusted_subnet_count" + WeaveIPAMTableID = "weave_ipam_table" + WeaveIPAMStatus = "weave_ipam_status" + WeaveIPAMRange = "weave_ipam_range" + WeaveIPAMDefaultSubnet = "weave_ipam_default_subnet" + WeaveDNSTableID = "weave_dns_table" + WeaveDNSDomain = "weave_dns_domain" + WeaveDNSUpstream = "weave_dns_upstream" + WeaveDNSTTL = "weave_dns_ttl" + WeaveDNSEntryCount = "weave_dns_entry_count" + WeaveProxyTableID = "weave_proxy_table" + WeaveProxyStatus = "weave_proxy_status" + WeaveProxyAddress = "weave_proxy_address" + WeavePluginTableID = "weave_plugin_table" + WeavePluginStatus = "weave_plugin_status" + WeavePluginDriver = "weave_plugin_driver" + WeaveConnectionsListPrefix = "weave_connections_list_" ) var ( @@ -72,16 +72,16 @@ var ( WeaveTrustedSubnets: {ID: WeaveTrustedSubnets, Label: "Trusted Subnets", From: report.FromSets, Priority: 9}, } - weaveTableTemplates = report.TableTemplates{ + weavePropertyListTemplates = report.PropertyListTemplates{ WeaveIPAMTableID: {ID: WeaveIPAMTableID, Label: "IPAM", - FixedRows: map[string]string{ + FixedProperties: map[string]string{ WeaveIPAMStatus: "Status", WeaveIPAMRange: "Range", WeaveIPAMDefaultSubnet: "Default Subnet", }, }, WeaveDNSTableID: {ID: WeaveDNSTableID, Label: "DNS", - FixedRows: map[string]string{ + FixedProperties: map[string]string{ WeaveDNSDomain: "Domain", WeaveDNSUpstream: "Upstream", WeaveDNSTTL: "TTL", @@ -89,21 +89,21 @@ var ( }, }, WeaveProxyTableID: {ID: WeaveProxyTableID, Label: "Proxy", - FixedRows: map[string]string{ + FixedProperties: map[string]string{ WeaveProxyStatus: "Status", WeaveProxyAddress: "Address", }, }, WeavePluginTableID: {ID: WeavePluginTableID, Label: "Plugin", - FixedRows: map[string]string{ + FixedProperties: map[string]string{ WeavePluginStatus: "Status", WeavePluginDriver: "Driver Name", }, }, - WeaveConnectionsTablePrefix: { - ID: WeaveConnectionsTablePrefix, + WeaveConnectionsListPrefix: { + ID: WeaveConnectionsListPrefix, Label: "Connections", - Prefix: WeaveConnectionsTablePrefix, + Prefix: WeaveConnectionsListPrefix, }, } ) @@ -345,7 +345,7 @@ func (w *Weave) Report() (report.Report, error) { r := report.MakeReport() r.Container = r.Container.WithMetadataTemplates(containerMetadata) - r.Overlay = r.Overlay.WithMetadataTemplates(weaveMetadata).WithTableTemplates(weaveTableTemplates) + r.Overlay = r.Overlay.WithMetadataTemplates(weaveMetadata).WithPropertyListTemplates(weavePropertyListTemplates) // We report nodes for all peers (not just the current node) to highlight peers not monitored by Scope // (i.e. without a running probe) @@ -434,13 +434,13 @@ func (w *Weave) addCurrentPeerInfo(latests map[string]string, node report.Node) latests[WeavePluginStatus] = "running" latests[WeavePluginDriver] = "weave" } - node = node.AddPrefixTable(WeaveConnectionsTablePrefix, getConnectionsTable(w.statusCache.Router)) + node = node.AddPrefixPropertyList(WeaveConnectionsListPrefix, getConnectionsList(w.statusCache.Router)) node = node.WithParents(report.EmptySets.Add(report.Host, report.MakeStringSet(w.hostID))) return latests, node } -func getConnectionsTable(router weave.Router) map[string]string { +func getConnectionsList(router weave.Router) map[string]string { const ( outboundArrow = "->" inboundArrow = "<-" diff --git a/render/detailed/tables.go b/render/detailed/property_lists.go similarity index 53% rename from render/detailed/tables.go rename to render/detailed/property_lists.go index 63fe918784..1f08937904 100644 --- a/render/detailed/tables.go +++ b/render/detailed/property_lists.go @@ -4,17 +4,16 @@ 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 { +// NodePropertyLists produces a list of property lists (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 NodePropertyLists(r report.Report, n report.Node) []report.PropertyList { 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 topology.PropertyListTemplates.PropertyLists(n) } return nil } diff --git a/render/detailed/tables_test.go b/render/detailed/property_lists_test.go similarity index 87% rename from render/detailed/tables_test.go rename to render/detailed/property_lists_test.go index 02f723391f..9ba4019db7 100644 --- a/render/detailed/tables_test.go +++ b/render/detailed/property_lists_test.go @@ -11,18 +11,18 @@ import ( "github.com/weaveworks/scope/test/fixture" ) -func TestNodeTables(t *testing.T) { +func TestNodePropertyLists(t *testing.T) { inputs := []struct { name string rpt report.Report node report.Node - want []report.Table + want []report.PropertyList }{ { name: "container", rpt: report.Report{ Container: report.MakeTopology(). - WithTableTemplates(docker.ContainerTableTemplates), + WithPropertyListTemplates(docker.ContainerPropertyListTemplates), }, node: report.MakeNodeWith(fixture.ClientContainerNodeID, map[string]string{ docker.ContainerID: fixture.ClientContainerID, @@ -31,7 +31,7 @@ func TestNodeTables(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.Table{ + want: []report.PropertyList{ { ID: docker.EnvPrefix, Label: "Environment Variables", @@ -65,7 +65,7 @@ func TestNodeTables(t *testing.T) { }, } for _, input := range inputs { - have := detailed.NodeTables(input.rpt, input.node) + have := detailed.NodePropertyLists(input.rpt, input.node) if !reflect.DeepEqual(input.want, have) { t.Errorf("%s: %s", input.name, test.Diff(input.want, have)) } diff --git a/render/detailed/summary.go b/render/detailed/summary.go index e4ad49e242..6baf7ba707 100644 --- a/render/detailed/summary.go +++ b/render/detailed/summary.go @@ -45,19 +45,19 @@ 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:"labelMinor"` - 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"` - Parents []Parent `json:"parents,omitempty"` - Metrics []report.MetricRow `json:"metrics,omitempty"` - Tables []report.Table `json:"tables,omitempty"` - Adjacency report.IDList `json:"adjacency,omitempty"` + ID string `json:"id"` + Label string `json:"label"` + LabelMinor string `json:"labelMinor"` + 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"` + Parents []Parent `json:"parents,omitempty"` + Metrics []report.MetricRow `json:"metrics,omitempty"` + PropertyLists []report.PropertyList `json:"propertyLists,omitempty"` + Adjacency report.IDList `json:"adjacency,omitempty"` } var renderers = map[string]func(NodeSummary, report.Node) (NodeSummary, bool){ @@ -106,14 +106,14 @@ func (n NodeSummary) SummarizeMetrics() NodeSummary { func baseNodeSummary(r report.Report, n report.Node) NodeSummary { t, _ := r.Topology(n.Topology) return NodeSummary{ - ID: n.ID, - Shape: t.GetShape(), - Linkable: true, - Metadata: NodeMetadata(r, n), - Metrics: NodeMetrics(r, n), - Parents: Parents(r, n), - Tables: NodeTables(r, n), - Adjacency: n.Adjacency, + ID: n.ID, + Shape: t.GetShape(), + Linkable: true, + Metadata: NodeMetadata(r, n), + Metrics: NodeMetrics(r, n), + Parents: Parents(r, n), + PropertyLists: NodePropertyLists(r, n), + Adjacency: n.Adjacency, } } diff --git a/report/property_list.go b/report/property_list.go new file mode 100644 index 0000000000..027f417754 --- /dev/null +++ b/report/property_list.go @@ -0,0 +1,191 @@ +package report + +import ( + "fmt" + "sort" + "strings" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/weaveworks/common/mtime" +) + +// MaxPropertyListSize sets the limit on the size of the property list to render. +// TODO: this won't be needed once we send reports incrementally +const ( + MaxPropertyListSize = 20 + TruncationCountPrefix = "property_list_truncation_count_" +) + +// AddPrefixPropertyList appends arbitrary key-value pairs to the Node, returning a new node. +func (node Node) AddPrefixPropertyList(prefix string, properties map[string]string) Node { + count := 0 + for key, value := range properties { + if count >= MaxPropertyListSize { + break + } + node = node.WithLatest(prefix+key, mtime.Now(), value) + count++ + } + if len(properties) > MaxPropertyListSize { + truncationCount := fmt.Sprintf("%d", len(properties)-MaxPropertyListSize) + node = node.WithLatest(TruncationCountPrefix+prefix, mtime.Now(), truncationCount) + } + return node +} + +// ExtractPropertyList returns the key-value pairs to build a property list from this node +func (node Node) ExtractPropertyList(template PropertyListTemplate) (rows map[string]string, truncationCount int) { + rows = map[string]string{} + truncationCount = 0 + node.Latest.ForEach(func(key string, _ time.Time, value string) { + if label, ok := template.FixedProperties[key]; ok { + rows[label] = value + } + if len(template.Prefix) > 0 && strings.HasPrefix(key, template.Prefix) { + label := key[len(template.Prefix):] + rows[label] = value + } + }) + if str, ok := node.Latest.Lookup(TruncationCountPrefix + template.Prefix); ok { + if n, err := fmt.Sscanf(str, "%d", &truncationCount); n != 1 || err != nil { + log.Warn("Unexpected truncation count format %q", str) + } + } + return rows, truncationCount +} + +// PropertyList is the type for a property list (labels) in the UI. +type PropertyList struct { + ID string `json:"id"` + Label string `json:"label"` + Rows []MetadataRow `json:"rows"` + TruncationCount int `json:"truncationCount,omitempty"` +} + +type propertyListsByID []PropertyList + +func (t propertyListsByID) Len() int { return len(t) } +func (t propertyListsByID) Swap(i, j int) { t[i], t[j] = t[j], t[i] } +func (t propertyListsByID) Less(i, j int) bool { return t[i].ID < t[j].ID } + +// Copy returns a copy of the PropertyList. +func (t PropertyList) Copy() PropertyList { + result := PropertyList{ + 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 +} + +// PropertyListTemplate describes how to render a property list for the UI. +type PropertyListTemplate struct { + ID string `json:"id"` + Label string `json:"label"` + Prefix string `json:"prefix"` + // FixedProperties indicates what predetermined properties to render each entry is + // indexed by the key to extract the row value is mapped to the row + // label + FixedProperties map[string]string `json:"fixedProperties"` +} + +// Copy returns a value-copy of the PropertyListTemplate +func (t PropertyListTemplate) Copy() PropertyListTemplate { + FixedPropertiesCopy := make(map[string]string, len(t.FixedProperties)) + for key, value := range t.FixedProperties { + FixedPropertiesCopy[key] = value + } + t.FixedProperties = FixedPropertiesCopy + return t +} + +// 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 PropertyListTemplate) Merge(other PropertyListTemplate) PropertyListTemplate { + max := func(s1, s2 string) string { + if s1 > s2 { + return s1 + } + return s2 + } + + fixedProperties := t.FixedProperties + if len(other.FixedProperties) > len(fixedProperties) { + fixedProperties = other.FixedProperties + } + + return PropertyListTemplate{ + ID: max(t.ID, other.ID), + Label: max(t.Label, other.Label), + Prefix: max(t.Prefix, other.Prefix), + FixedProperties: fixedProperties, + } +} + +// PropertyListTemplates is a mergeable set of PropertyListTemplate +type PropertyListTemplates map[string]PropertyListTemplate + +// PropertyLists renders the PropertyListTemplates for a given node. +func (t PropertyListTemplates) PropertyLists(node Node) []PropertyList { + var result []PropertyList + for _, template := range t { + rows, truncationCount := node.ExtractPropertyList(template) + propertyList := PropertyList{ + ID: template.ID, + Label: template.Label, + Rows: []MetadataRow{}, + TruncationCount: truncationCount, + } + keys := make([]string, 0, len(rows)) + for k := range rows { + keys = append(keys, k) + } + sort.Strings(keys) + for _, key := range keys { + propertyList.Rows = append(propertyList.Rows, MetadataRow{ + ID: "label_" + key, + Label: key, + Value: rows[key], + }) + } + result = append(result, propertyList) + } + sort.Sort(propertyListsByID(result)) + return result +} + +// Copy returns a value copy of the PropertyListTemplates +func (t PropertyListTemplates) Copy() PropertyListTemplates { + if t == nil { + return nil + } + result := PropertyListTemplates{} + for k, v := range t { + result[k] = v.Copy() + } + return result +} + +// Merge merges two sets of PropertyListTemplates +func (t PropertyListTemplates) Merge(other PropertyListTemplates) PropertyListTemplates { + if t == nil && other == nil { + return nil + } + result := make(PropertyListTemplates, len(t)) + for k, v := range t { + result[k] = v + } + for k, v := range other { + if existing, ok := result[k]; ok { + result[k] = v.Merge(existing) + } else { + result[k] = v + } + } + return result +} diff --git a/report/table_test.go b/report/property_list_test.go similarity index 70% rename from report/table_test.go rename to report/property_list_test.go index 72b6d94cd4..1a73168d62 100644 --- a/report/table_test.go +++ b/report/property_list_test.go @@ -16,8 +16,8 @@ func TestPrefixTables(t *testing.T) { } nmd := report.MakeNode("foo1") - nmd = nmd.AddPrefixTable("foo_", want) - have, truncationCount := nmd.ExtractTable(report.TableTemplate{Prefix: "foo_"}) + nmd = nmd.AddPrefixPropertyList("foo_", want) + have, truncationCount := nmd.ExtractPropertyList(report.PropertyListTemplate{Prefix: "foo_"}) if truncationCount != 0 { t.Error("Table shouldn't had been truncated") @@ -38,13 +38,13 @@ func TestFixedTables(t *testing.T) { "foo2key": "bar2", }) - template := report.TableTemplate{FixedRows: map[string]string{ + template := report.PropertyListTemplate{FixedProperties: map[string]string{ "foo1key": "foo1", "foo2key": "foo2", }, } - have, _ := nmd.ExtractTable(template) + have, _ := nmd.ExtractPropertyList(template) if !reflect.DeepEqual(want, have) { t.Error(test.Diff(want, have)) @@ -54,7 +54,7 @@ func TestFixedTables(t *testing.T) { func TestTruncation(t *testing.T) { wantTruncationCount := 1 want := map[string]string{} - for i := 0; i < report.MaxTableRows+wantTruncationCount; i++ { + for i := 0; i < report.MaxPropertyListSize+wantTruncationCount; i++ { key := fmt.Sprintf("key%d", i) value := fmt.Sprintf("value%d", i) want[key] = value @@ -62,8 +62,8 @@ func TestTruncation(t *testing.T) { nmd := report.MakeNode("foo1") - nmd = nmd.AddPrefixTable("foo_", want) - _, truncationCount := nmd.ExtractTable(report.TableTemplate{Prefix: "foo_"}) + nmd = nmd.AddPrefixPropertyList("foo_", want) + _, truncationCount := nmd.ExtractPropertyList(report.PropertyListTemplate{Prefix: "foo_"}) if truncationCount != wantTruncationCount { t.Error( diff --git a/report/table.go b/report/table.go deleted file mode 100644 index 74cbc1a71f..0000000000 --- a/report/table.go +++ /dev/null @@ -1,198 +0,0 @@ -package report - -import ( - "fmt" - "sort" - "strings" - "time" - - log "github.com/Sirupsen/logrus" - "github.com/weaveworks/common/mtime" -) - -// MaxTableRows sets the limit on the table size to render -// TODO: this won't be needed once we send reports incrementally -const ( - MaxTableRows = 20 - TruncationCountPrefix = "table_truncation_count_" -) - -// AddPrefixTable appends arbitrary key-value pairs to the Node, returning a new node. -func (node Node) AddPrefixTable(prefix string, labels map[string]string) Node { - count := 0 - for key, value := range labels { - if count >= MaxTableRows { - break - } - node = node.WithLatest(prefix+key, mtime.Now(), value) - count++ - } - if len(labels) > MaxTableRows { - truncationCount := fmt.Sprintf("%d", len(labels)-MaxTableRows) - node = node.WithLatest(TruncationCountPrefix+prefix, mtime.Now(), truncationCount) - } - return node -} - -// ExtractTable returns the key-value pairs to build a table from this node -func (node Node) ExtractTable(template TableTemplate) (rows map[string]string, truncationCount int) { - rows = map[string]string{} - truncationCount = 0 - node.Latest.ForEach(func(key string, _ time.Time, value string) { - if label, ok := template.FixedRows[key]; ok { - rows[label] = value - } - if len(template.Prefix) > 0 && strings.HasPrefix(key, template.Prefix) { - label := key[len(template.Prefix):] - rows[label] = value - } - }) - if str, ok := node.Latest.Lookup(TruncationCountPrefix + template.Prefix); ok { - if n, err := fmt.Sscanf(str, "%d", &truncationCount); n != 1 || err != nil { - log.Warn("Unexpected truncation count format %q", str) - } - } - return rows, truncationCount -} - -// 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"` - TruncationCount int `json:"truncationCount,omitempty"` -} - -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 -} - -// FixedRow describes a row which is part of a TableTemplate and whose value is extracted -// from a predetermined key -type FixedRow struct { - Label string `json:"label"` - Key string `json:"key"` -} - -// 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"` - // FixedRows indicates what predetermined rows to render each entry is - // indexed by the key to extract the row value is mapped to the row - // label - FixedRows map[string]string `json:"fixedRows"` -} - -// Copy returns a value-copy of the TableTemplate -func (t TableTemplate) Copy() TableTemplate { - fixedRowsCopy := make(map[string]string, len(t.FixedRows)) - for key, value := range t.FixedRows { - fixedRowsCopy[key] = value - } - t.FixedRows = fixedRowsCopy - return t -} - -// 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 - } - - fixedRows := t.FixedRows - if len(other.FixedRows) > len(fixedRows) { - fixedRows = other.FixedRows - } - - return TableTemplate{ - ID: max(t.ID, other.ID), - Label: max(t.Label, other.Label), - Prefix: max(t.Prefix, other.Prefix), - FixedRows: fixedRows, - } -} - -// 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 { - rows, truncationCount := node.ExtractTable(template) - table := Table{ - ID: template.ID, - Label: template.Label, - Rows: []MetadataRow{}, - TruncationCount: truncationCount, - } - 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 { - if t == nil && other == nil { - return nil - } - result := make(TableTemplates, len(t)) - for k, v := range t { - result[k] = v - } - for k, v := range other { - if existing, ok := result[k]; ok { - result[k] = v.Merge(existing) - } else { - result[k] = v - } - } - return result -} diff --git a/report/topology.go b/report/topology.go index 1beba4f79d..6fa58321bb 100644 --- a/report/topology.go +++ b/report/topology.go @@ -10,14 +10,14 @@ import ( // EdgeMetadatas and Nodes respectively. Edges are directional, and embedded // in the Node struct. type Topology struct { - Shape string `json:"shape,omitempty"` - Label string `json:"label,omitempty"` - LabelPlural string `json:"label_plural,omitempty"` - Nodes `json:"nodes"` - Controls `json:"controls,omitempty"` - MetadataTemplates `json:"metadata_templates,omitempty"` - MetricTemplates `json:"metric_templates,omitempty"` - TableTemplates `json:"table_templates,omitempty"` + Shape string `json:"shape,omitempty"` + Label string `json:"label,omitempty"` + LabelPlural string `json:"label_plural,omitempty"` + Nodes `json:"nodes"` + Controls `json:"controls,omitempty"` + MetadataTemplates `json:"metadata_templates,omitempty"` + MetricTemplates `json:"metric_templates,omitempty"` + PropertyListTemplates `json:"property_list_templates,omitempty"` } // MakeTopology gives you a Topology. @@ -32,14 +32,14 @@ func MakeTopology() Topology { // returning a new topology. func (t Topology) WithMetadataTemplates(other MetadataTemplates) Topology { return Topology{ - Shape: t.Shape, - Label: t.Label, - LabelPlural: t.LabelPlural, - Nodes: t.Nodes.Copy(), - Controls: t.Controls.Copy(), - MetadataTemplates: t.MetadataTemplates.Merge(other), - MetricTemplates: t.MetricTemplates.Copy(), - TableTemplates: t.TableTemplates.Copy(), + Shape: t.Shape, + Label: t.Label, + LabelPlural: t.LabelPlural, + Nodes: t.Nodes.Copy(), + Controls: t.Controls.Copy(), + MetadataTemplates: t.MetadataTemplates.Merge(other), + MetricTemplates: t.MetricTemplates.Copy(), + PropertyListTemplates: t.PropertyListTemplates.Copy(), } } @@ -47,57 +47,57 @@ func (t Topology) WithMetadataTemplates(other MetadataTemplates) Topology { // returning a new topology. func (t Topology) WithMetricTemplates(other MetricTemplates) Topology { return Topology{ - Shape: t.Shape, - Label: t.Label, - LabelPlural: t.LabelPlural, - Nodes: t.Nodes.Copy(), - Controls: t.Controls.Copy(), - MetadataTemplates: t.MetadataTemplates.Copy(), - MetricTemplates: t.MetricTemplates.Merge(other), - TableTemplates: t.TableTemplates.Copy(), + Shape: t.Shape, + Label: t.Label, + LabelPlural: t.LabelPlural, + Nodes: t.Nodes.Copy(), + Controls: t.Controls.Copy(), + MetadataTemplates: t.MetadataTemplates.Copy(), + MetricTemplates: t.MetricTemplates.Merge(other), + PropertyListTemplates: t.PropertyListTemplates.Copy(), } } -// WithTableTemplates merges some table templates into this topology, +// WithPropertyListTemplates merges some table templates into this topology, // returning a new topology. -func (t Topology) WithTableTemplates(other TableTemplates) Topology { +func (t Topology) WithPropertyListTemplates(other PropertyListTemplates) Topology { return Topology{ - Shape: t.Shape, - Label: t.Label, - LabelPlural: t.LabelPlural, - Nodes: t.Nodes.Copy(), - Controls: t.Controls.Copy(), - MetadataTemplates: t.MetadataTemplates.Copy(), - MetricTemplates: t.MetricTemplates.Copy(), - TableTemplates: t.TableTemplates.Merge(other), + Shape: t.Shape, + Label: t.Label, + LabelPlural: t.LabelPlural, + Nodes: t.Nodes.Copy(), + Controls: t.Controls.Copy(), + MetadataTemplates: t.MetadataTemplates.Copy(), + MetricTemplates: t.MetricTemplates.Copy(), + PropertyListTemplates: t.PropertyListTemplates.Merge(other), } } // WithShape sets the shape of nodes from this topology, returning a new topology. func (t Topology) WithShape(shape string) Topology { return Topology{ - Shape: shape, - Label: t.Label, - LabelPlural: t.LabelPlural, - Nodes: t.Nodes.Copy(), - Controls: t.Controls.Copy(), - MetadataTemplates: t.MetadataTemplates.Copy(), - MetricTemplates: t.MetricTemplates.Copy(), - TableTemplates: t.TableTemplates.Copy(), + Shape: shape, + Label: t.Label, + LabelPlural: t.LabelPlural, + Nodes: t.Nodes.Copy(), + Controls: t.Controls.Copy(), + MetadataTemplates: t.MetadataTemplates.Copy(), + MetricTemplates: t.MetricTemplates.Copy(), + PropertyListTemplates: t.PropertyListTemplates.Copy(), } } // WithLabel sets the label terminology of this topology, returning a new topology. func (t Topology) WithLabel(label, labelPlural string) Topology { return Topology{ - Shape: t.Shape, - Label: label, - LabelPlural: labelPlural, - Nodes: t.Nodes.Copy(), - Controls: t.Controls.Copy(), - MetadataTemplates: t.MetadataTemplates.Copy(), - MetricTemplates: t.MetricTemplates.Copy(), - TableTemplates: t.TableTemplates.Copy(), + Shape: t.Shape, + Label: label, + LabelPlural: labelPlural, + Nodes: t.Nodes.Copy(), + Controls: t.Controls.Copy(), + MetadataTemplates: t.MetadataTemplates.Copy(), + MetricTemplates: t.MetricTemplates.Copy(), + PropertyListTemplates: t.PropertyListTemplates.Copy(), } } @@ -125,14 +125,14 @@ func (t Topology) GetShape() string { // Copy returns a value copy of the Topology. func (t Topology) Copy() Topology { return Topology{ - Shape: t.Shape, - Label: t.Label, - LabelPlural: t.LabelPlural, - Nodes: t.Nodes.Copy(), - Controls: t.Controls.Copy(), - MetadataTemplates: t.MetadataTemplates.Copy(), - MetricTemplates: t.MetricTemplates.Copy(), - TableTemplates: t.TableTemplates.Copy(), + Shape: t.Shape, + Label: t.Label, + LabelPlural: t.LabelPlural, + Nodes: t.Nodes.Copy(), + Controls: t.Controls.Copy(), + MetadataTemplates: t.MetadataTemplates.Copy(), + MetricTemplates: t.MetricTemplates.Copy(), + PropertyListTemplates: t.PropertyListTemplates.Copy(), } } @@ -148,14 +148,14 @@ func (t Topology) Merge(other Topology) Topology { label, labelPlural = other.Label, other.LabelPlural } return Topology{ - Shape: shape, - Label: label, - LabelPlural: labelPlural, - Nodes: t.Nodes.Merge(other.Nodes), - Controls: t.Controls.Merge(other.Controls), - MetadataTemplates: t.MetadataTemplates.Merge(other.MetadataTemplates), - MetricTemplates: t.MetricTemplates.Merge(other.MetricTemplates), - TableTemplates: t.TableTemplates.Merge(other.TableTemplates), + Shape: shape, + Label: label, + LabelPlural: labelPlural, + Nodes: t.Nodes.Merge(other.Nodes), + Controls: t.Controls.Merge(other.Controls), + MetadataTemplates: t.MetadataTemplates.Merge(other.MetadataTemplates), + MetricTemplates: t.MetricTemplates.Merge(other.MetricTemplates), + PropertyListTemplates: t.PropertyListTemplates.Merge(other.PropertyListTemplates), } }