diff --git a/.changelog/17754.txt b/.changelog/17754.txt new file mode 100644 index 0000000000..32272ec1ae --- /dev/null +++ b/.changelog/17754.txt @@ -0,0 +1,3 @@ +```release-note:feature +ui: Display the Consul agent version in the nodes list, and allow filtering and sorting of nodes based on versions. +``` \ No newline at end of file diff --git a/agent/agent.go b/agent/agent.go index fa75a1cd1c..881b94209d 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -3999,6 +3999,7 @@ func (a *Agent) loadMetadata(conf *config.RuntimeConfig) error { meta[k] = v } meta[structs.MetaSegmentKey] = conf.SegmentName + meta[structs.MetaConsulVersion] = conf.Version return a.State.LoadMetadata(meta) } diff --git a/agent/agent_endpoint_test.go b/agent/agent_endpoint_test.go index c465b687a8..1a275f61af 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -1506,7 +1506,8 @@ func TestAgent_Self(t *testing.T) { require.NoError(t, err) require.Equal(t, cs[a.config.SegmentName], val.Coord) - delete(val.Meta, structs.MetaSegmentKey) // Added later, not in config. + delete(val.Meta, structs.MetaSegmentKey) // Added later, not in config. + delete(val.Meta, structs.MetaConsulVersion) // Added later, not in config. require.Equal(t, a.config.NodeMeta, val.Meta) if tc.expectXDS { diff --git a/agent/consul/leader.go b/agent/consul/leader.go index c91655c5c8..4bc1908d5f 100644 --- a/agent/consul/leader.go +++ b/agent/consul/leader.go @@ -1087,6 +1087,13 @@ AFTER_CHECK: "partition", getSerfMemberEnterpriseMeta(member).PartitionOrDefault(), ) + // Get consul version from serf member + // add this as node meta in catalog register request + buildVersion, err := metadata.Build(&member) + if err != nil { + return err + } + // Register with the catalog. req := structs.RegisterRequest{ Datacenter: s.config.Datacenter, @@ -1102,6 +1109,9 @@ AFTER_CHECK: Output: structs.SerfCheckAliveOutput, }, EnterpriseMeta: *nodeEntMeta, + NodeMeta: map[string]string{ + structs.MetaConsulVersion: buildVersion.String(), + }, } if node != nil { req.TaggedAddresses = node.TaggedAddresses diff --git a/agent/consul/state/catalog.go b/agent/consul/state/catalog.go index 74efc32295..4e9fcf716c 100644 --- a/agent/consul/state/catalog.go +++ b/agent/consul/state/catalog.go @@ -3450,6 +3450,13 @@ func parseNodes(tx ReadTxn, ws memdb.WatchSet, idx uint64, ws.AddWithLimit(watchLimit, services.WatchCh(), allServicesCh) for service := services.Next(); service != nil; service = services.Next() { ns := service.(*structs.ServiceNode).ToNodeService() + // If version isn't defined in node meta, set it from the Consul service meta + if _, ok := dump.Meta[structs.MetaConsulVersion]; !ok && ns.ID == "consul" && ns.Meta["version"] != "" { + if dump.Meta == nil { + dump.Meta = make(map[string]string) + } + dump.Meta[structs.MetaConsulVersion] = ns.Meta["version"] + } dump.Services = append(dump.Services, ns) } diff --git a/agent/consul/state/catalog_test.go b/agent/consul/state/catalog_test.go index 0de535c3b4..e6b279580b 100644 --- a/agent/consul/state/catalog_test.go +++ b/agent/consul/state/catalog_test.go @@ -4837,6 +4837,9 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) { } // Register some nodes + // node1 is registered withOut any nodemeta, and a consul service with id + // 'consul' is added later with meta 'version'. The expected node must have + // meta 'consul-version' with same value testRegisterNode(t, s, 0, "node1") testRegisterNode(t, s, 1, "node2") @@ -4845,6 +4848,8 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) { testRegisterService(t, s, 3, "node1", "service2") testRegisterService(t, s, 4, "node2", "service1") testRegisterService(t, s, 5, "node2", "service2") + // Register consul service with meta 'version' for node1 + testRegisterServiceWithMeta(t, s, 10, "node1", "consul", map[string]string{"version": "1.17.0"}) // Register service-level checks testRegisterCheck(t, s, 6, "node1", "service1", "check1", api.HealthPassing) @@ -4894,6 +4899,19 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) { }, }, Services: []*structs.NodeService{ + { + ID: "consul", + Service: "consul", + Address: "1.1.1.1", + Meta: map[string]string{"version": "1.17.0"}, + Port: 1111, + Weights: &structs.Weights{Passing: 1, Warning: 1}, + RaftIndex: structs.RaftIndex{ + CreateIndex: 10, + ModifyIndex: 10, + }, + EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), + }, { ID: "service1", Service: "service1", @@ -4921,6 +4939,7 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) { EnterpriseMeta: *structs.DefaultEnterpriseMetaInDefaultPartition(), }, }, + Meta: map[string]string{"consul-version": "1.17.0"}, }, &structs.NodeInfo{ Node: "node2", @@ -4988,7 +5007,7 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - if idx != 9 { + if idx != 10 { t.Fatalf("bad index: %d", idx) } require.Len(t, dump, 1) @@ -4999,8 +5018,8 @@ func TestStateStore_NodeInfo_NodeDump(t *testing.T) { if err != nil { t.Fatalf("err: %s", err) } - if idx != 9 { - t.Fatalf("bad index: %d", 9) + if idx != 10 { + t.Fatalf("bad index: %d", idx) } if !reflect.DeepEqual(dump, expect) { t.Fatalf("bad: %#v", dump[0].Services[0]) diff --git a/agent/consul/state/state_store_test.go b/agent/consul/state/state_store_test.go index fef7502532..587f15c03d 100644 --- a/agent/consul/state/state_store_test.go +++ b/agent/consul/state/state_store_test.go @@ -189,6 +189,37 @@ func testRegisterServiceWithChangeOpts(t *testing.T, s *Store, idx uint64, nodeI return svc } +// testRegisterServiceWithMeta registers service with Meta passed as arg. +func testRegisterServiceWithMeta(t *testing.T, s *Store, idx uint64, nodeID, serviceID string, meta map[string]string, opts ...func(service *structs.NodeService)) *structs.NodeService { + svc := &structs.NodeService{ + ID: serviceID, + Service: serviceID, + Address: "1.1.1.1", + Port: 1111, + Meta: meta, + } + for _, o := range opts { + o(svc) + } + + if err := s.EnsureService(idx, nodeID, svc); err != nil { + t.Fatalf("err: %s", err) + } + + tx := s.db.Txn(false) + defer tx.Abort() + service, err := tx.First(tableServices, indexID, NodeServiceQuery{Node: nodeID, Service: serviceID, PeerName: svc.PeerName}) + if err != nil { + t.Fatalf("err: %s", err) + } + if result, ok := service.(*structs.ServiceNode); !ok || + result.Node != nodeID || + result.ServiceID != serviceID { + t.Fatalf("bad service: %#v", result) + } + return svc +} + // testRegisterService register a service with given transaction idx // If the service already exists, transaction number might not be increased // Use `testRegisterServiceWithChange()` if you want perform a registration that diff --git a/agent/local/state_test.go b/agent/local/state_test.go index 0a78f321f7..4751352ec1 100644 --- a/agent/local/state_test.go +++ b/agent/local/state_test.go @@ -189,7 +189,8 @@ func TestAgentAntiEntropy_Services(t *testing.T) { id := services.NodeServices.Node.ID addrs := services.NodeServices.Node.TaggedAddresses meta := services.NodeServices.Node.Meta - delete(meta, structs.MetaSegmentKey) // Added later, not in config. + delete(meta, structs.MetaSegmentKey) // Added later, not in config. + delete(meta, structs.MetaConsulVersion) // Added later, not in config. assert.Equal(t, a.Config.NodeID, id) assert.Equal(t, a.Config.TaggedAddresses, addrs) assert.Equal(t, unNilMap(a.Config.NodeMeta), meta) @@ -1355,7 +1356,8 @@ func TestAgentAntiEntropy_Checks(t *testing.T) { id := services.NodeServices.Node.ID addrs := services.NodeServices.Node.TaggedAddresses meta := services.NodeServices.Node.Meta - delete(meta, structs.MetaSegmentKey) // Added later, not in config. + delete(meta, structs.MetaSegmentKey) // Added later, not in config. + delete(meta, structs.MetaConsulVersion) // Added later, not in config. assert.Equal(r, a.Config.NodeID, id) assert.Equal(r, a.Config.TaggedAddresses, addrs) assert.Equal(r, unNilMap(a.Config.NodeMeta), meta) @@ -2016,7 +2018,8 @@ func TestAgentAntiEntropy_NodeInfo(t *testing.T) { addrs := services.NodeServices.Node.TaggedAddresses meta := services.NodeServices.Node.Meta nodeLocality := services.NodeServices.Node.Locality - delete(meta, structs.MetaSegmentKey) // Added later, not in config. + delete(meta, structs.MetaSegmentKey) // Added later, not in config. + delete(meta, structs.MetaConsulVersion) // Added later, not in config. require.Equal(t, a.Config.NodeID, id) require.Equal(t, a.Config.TaggedAddresses, addrs) require.Equal(t, a.Config.StructLocality(), nodeLocality) @@ -2041,7 +2044,8 @@ func TestAgentAntiEntropy_NodeInfo(t *testing.T) { addrs := services.NodeServices.Node.TaggedAddresses meta := services.NodeServices.Node.Meta nodeLocality := services.NodeServices.Node.Locality - delete(meta, structs.MetaSegmentKey) // Added later, not in config. + delete(meta, structs.MetaSegmentKey) // Added later, not in config. + delete(meta, structs.MetaConsulVersion) // Added later, not in config. require.Equal(t, nodeID, id) require.Equal(t, a.Config.TaggedAddresses, addrs) require.Equal(t, a.Config.StructLocality(), nodeLocality) diff --git a/agent/structs/structs.go b/agent/structs/structs.go index 59385fa5ba..096f767a47 100644 --- a/agent/structs/structs.go +++ b/agent/structs/structs.go @@ -220,6 +220,9 @@ const ( // WildcardSpecifier is the string which should be used for specifying a wildcard // The exact semantics of the wildcard is left up to the code where its used. WildcardSpecifier = "*" + + // MetaConsulVersion is the node metadata key used to store the node's consul version + MetaConsulVersion = "consul-version" ) var allowedConsulMetaKeysForMeshGateway = map[string]struct{}{MetaWANFederationKey: {}} diff --git a/agent/ui_endpoint.go b/agent/ui_endpoint.go index 3de9eac155..8f51849696 100644 --- a/agent/ui_endpoint.go +++ b/agent/ui_endpoint.go @@ -13,9 +13,12 @@ import ( "strings" "github.com/hashicorp/go-hclog" + "github.com/hashicorp/serf/serf" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/config" + "github.com/hashicorp/consul/agent/consul" + "github.com/hashicorp/consul/agent/metadata" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/logging" @@ -110,7 +113,18 @@ RPC: return nil, err } + // Get version info for all serf members into a map of key-address,value-version. + // This logic of calling 'AgentMembersMapAddrVer()' and inserting version info in this func + // can be discarded in future releases ( may be after 3 or 4 minor releases), + // when all the nodes are registered with consul-version in nodemeta. + var err error + mapAddrVer, err := AgentMembersMapAddrVer(s, req) + if err != nil { + return nil, err + } + // Use empty list instead of nil + // Also check if consul-version exists in Meta, else add it for _, info := range out.Dump { if info.Services == nil { info.Services = make([]*structs.NodeService, 0) @@ -118,12 +132,24 @@ RPC: if info.Checks == nil { info.Checks = make([]*structs.HealthCheck, 0) } + // Check if Node Meta - 'consul-version' already exists by virtue of adding + // 'consul-version' during node registration itself. + // If not, get it from mapAddrVer. + if _, ok := info.Meta[structs.MetaConsulVersion]; !ok { + if _, okver := mapAddrVer[info.Address]; okver { + if info.Meta == nil { + info.Meta = make(map[string]string) + } + info.Meta[structs.MetaConsulVersion] = mapAddrVer[info.Address] + } + } } if out.Dump == nil { out.Dump = make(structs.NodeDump, 0) } // Use empty list instead of nil + // Also check if consul-version exists in Meta, else add it for _, info := range out.ImportedDump { if info.Services == nil { info.Services = make([]*structs.NodeService, 0) @@ -131,11 +157,60 @@ RPC: if info.Checks == nil { info.Checks = make([]*structs.HealthCheck, 0) } + // Check if Node Meta - 'consul-version' already exists by virtue of adding + // 'consul-version' during node registration itself. + // If not, get it from mapAddrVer. + if _, ok := info.Meta[structs.MetaConsulVersion]; !ok { + if _, okver := mapAddrVer[info.Address]; okver { + if info.Meta == nil { + info.Meta = make(map[string]string) + } + info.Meta[structs.MetaConsulVersion] = mapAddrVer[info.Address] + } + } } return append(out.Dump, out.ImportedDump...), nil } +// AgentMembersMapAddrVer is used to get version info from all serf members into a +// map of key-address,value-version. +func AgentMembersMapAddrVer(s *HTTPHandlers, req *http.Request) (map[string]string, error) { + var members []serf.Member + + //Get WAN Members + wanMembers := s.agent.WANMembers() + + //Get LAN Members + //Get the request partition and default to that of the agent. + entMeta := s.agent.AgentEnterpriseMeta() + if err := s.parseEntMetaPartition(req, entMeta); err != nil { + return nil, err + } + filter := consul.LANMemberFilter{ + Partition: entMeta.PartitionOrDefault(), + } + filter.AllSegments = true + lanMembers, err := s.agent.delegate.LANMembers(filter) + if err != nil { + return nil, err + } + + //aggregate members + members = append(wanMembers, lanMembers...) + + //create a map with key as IPv4 address and value as consul-version + mapAddrVer := make(map[string]string, len(members)) + for i := range members { + buildVersion, err := metadata.Build(&members[i]) + if err == nil { + mapAddrVer[members[i].Addr.String()] = buildVersion.String() + } + } + + return mapAddrVer, nil +} + // UINodeInfo is used to get info on a single node in a given datacenter. We return a // NodeInfo which provides overview information for the node func (s *HTTPHandlers) UINodeInfo(resp http.ResponseWriter, req *http.Request) (interface{}, error) { @@ -172,6 +247,16 @@ RPC: return nil, err } + // Get version info for all serf members into a map of key-address,value-version. + // This logic of calling 'AgentMembersMapAddrVer()' and inserting version info in this func + // can be discarded in future releases ( may be after 3 or 4 minor releases), + // when all the nodes are registered with consul-version in nodemeta. + var err error + mapAddrVer, err := AgentMembersMapAddrVer(s, req) + if err != nil { + return nil, err + } + // Return only the first entry if len(out.Dump) > 0 { info := out.Dump[0] @@ -181,6 +266,17 @@ RPC: if info.Checks == nil { info.Checks = make([]*structs.HealthCheck, 0) } + // Check if Node Meta - 'consul-version' already exists by virtue of adding + // 'consul-version' during node registration itself. + // If not, get it from mapAddrVer. + if _, ok := info.Meta[structs.MetaConsulVersion]; !ok { + if _, okver := mapAddrVer[info.Address]; okver { + if info.Meta == nil { + info.Meta = make(map[string]string) + } + info.Meta[structs.MetaConsulVersion] = mapAddrVer[info.Address] + } + } return info, nil } diff --git a/agent/ui_endpoint_test.go b/agent/ui_endpoint_test.go index 5fc2e06d34..f6810db801 100644 --- a/agent/ui_endpoint_test.go +++ b/agent/ui_endpoint_test.go @@ -162,6 +162,9 @@ func TestUINodes(t *testing.T) { require.Len(t, nodes[2].Services, 0) require.NotNil(t, nodes[1].Checks) require.Len(t, nodes[2].Services, 0) + + // check for consul-version in node meta + require.Equal(t, nodes[0].Meta[structs.MetaConsulVersion], a.Config.Version) } func TestUINodes_Filter(t *testing.T) { @@ -260,6 +263,9 @@ func TestUINodeInfo(t *testing.T) { node.Checks == nil || len(node.Checks) != 0 { t.Fatalf("bad: %v", node) } + + // check for consul-version in node meta + require.Equal(t, node.Meta[structs.MetaConsulVersion], a.Config.Version) } func TestUIServices(t *testing.T) { diff --git a/api/catalog_test.go b/api/catalog_test.go index 6226691353..2b0a4097b3 100644 --- a/api/catalog_test.go +++ b/api/catalog_test.go @@ -65,6 +65,7 @@ func TestAPI_CatalogNodes(t *testing.T) { }, Meta: map[string]string{ "consul-network-segment": "", + "consul-version": s.Config.Version, }, } require.Equal(r, want, got) diff --git a/api/txn_test.go b/api/txn_test.go index 975f3e3816..ea454976da 100644 --- a/api/txn_test.go +++ b/api/txn_test.go @@ -361,7 +361,10 @@ func TestAPI_ClientTxn(t *testing.T) { "wan": s.Config.Bind, "wan_ipv4": s.Config.Bind, }, - Meta: map[string]string{"consul-network-segment": ""}, + Meta: map[string]string{ + "consul-network-segment": "", + "consul-version": s.Config.Version, + }, CreateIndex: ret.Results[1].Node.CreateIndex, ModifyIndex: ret.Results[1].Node.ModifyIndex, }, diff --git a/sdk/testutil/server.go b/sdk/testutil/server.go index d00850d5e1..a20f95123a 100644 --- a/sdk/testutil/server.go +++ b/sdk/testutil/server.go @@ -130,6 +130,7 @@ type TestServerConfig struct { Args []string `json:"-"` ReturnPorts func() `json:"-"` Audit *TestAuditConfig `json:"audit,omitempty"` + Version string `json:"version,omitempty"` } type TestACLs struct { @@ -212,6 +213,7 @@ func defaultServerConfig(t TestingTB, consulVersion *version.Version) *TestServe Stdout: logBuffer, Stderr: logBuffer, Peering: &TestPeeringConfig{Enabled: true}, + Version: consulVersion.String(), } // Add version-specific tweaks diff --git a/ui/packages/consul-ui/app/components/consul/node/list/index.hbs b/ui/packages/consul-ui/app/components/consul/node/list/index.hbs index c1392bd686..f57d7d5ab0 100644 --- a/ui/packages/consul-ui/app/components/consul/node/list/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/node/list/index.hbs @@ -50,5 +50,17 @@ as |item index|> {{item.Address}} +