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:
Tom Wilkie
2016-04-20 08:18:03 +01:00
parent b745411ce1
commit 9eda27822c
18 changed files with 340 additions and 221 deletions

View File

@@ -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>
);

View File

@@ -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>
);
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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))
}
}

View File

@@ -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
}

View File

@@ -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]})

View File

@@ -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)
}

View File

@@ -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{}
)

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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
View 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
}

View File

@@ -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
View 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
}

View File

@@ -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))

View File

@@ -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),
}
}