mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-02 01:30:30 +00:00
Show k8s labels and container env vars in the details panel. (#1342)
* Show k8s labels and container env vars in the details panel. * Add show more bar to the env vars and labels * React key was in the wrong place; empty tables render section labels.
This commit is contained in:
@@ -191,11 +191,17 @@ export default class NodeDetails extends React.Component {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{details.docker_labels && details.docker_labels.length > 0
|
||||
&& <div className="node-details-content-section">
|
||||
<div className="node-details-content-section-header">Docker Labels</div>
|
||||
<NodeDetailsLabels rows={details.docker_labels} />
|
||||
</div>}
|
||||
{details.tables && details.tables.length > 0 && details.tables.map(table => {
|
||||
if (table.rows.length > 0) {
|
||||
return (
|
||||
<div className="node-details-content-section" key={table.id}>
|
||||
<div className="node-details-content-section-header">{table.label}</div>
|
||||
<NodeDetailsLabels rows={table.rows} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function NodeDetailsLabels({rows}) {
|
||||
return (
|
||||
<div className="node-details-labels">
|
||||
{rows.map(field => (<div className="node-details-labels-field" key={field.id}>
|
||||
<div className="node-details-labels-field-label truncate" title={field.label}>
|
||||
{field.label}
|
||||
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 (
|
||||
<div className="node-details-labels">
|
||||
{rows.map(field => (<div className="node-details-labels-field" key={field.id}>
|
||||
<div className="node-details-labels-field-label truncate" title={field.label}
|
||||
key={field.id}>
|
||||
{field.label}
|
||||
</div>
|
||||
<div className="node-details-labels-field-value truncate" title={field.value}>
|
||||
{field.value}
|
||||
</div>
|
||||
</div>
|
||||
<div className="node-details-labels-field-value truncate" title={field.value}>
|
||||
{field.value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
))}
|
||||
<ShowMore handleClick={this.handleLimitClick} collection={this.props.rows}
|
||||
expanded={expanded} notShown={notShown} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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{}
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
20
render/detailed/tables.go
Normal file
20
render/detailed/tables.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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: "label_label1",
|
||||
Label: "label1",
|
||||
Value: "label1value",
|
||||
ID: docker.EnvPrefix,
|
||||
Label: "Environment Variables",
|
||||
Rows: []report.MetadataRow{},
|
||||
},
|
||||
{
|
||||
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))
|
||||
}
|
||||
146
report/table.go
Normal file
146
report/table.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user