Skip to content

Commit

Permalink
Displays Consul version of each nodes in UI nodes section (#17754)
Browse files Browse the repository at this point in the history
* update UINodes and UINodeInfo response with consul-version info added as NodeMeta, fetched from serf members

* update test cases TestUINodes, TestUINodeInfo

* added nil check for map

* add consul-version in local agent node metadata

* get consul version from serf member and add this as node meta in catalog register request

* updated ui mock response to include consul versions as node meta

* updated ui trans and added version as query param to node list route

* updates in ui templates to display consul version with filter and sorts

* updates in ui - model class, serializers,comparators,predicates for consul version feature

* added change log for Consul Version Feature

* updated to get version from consul service, if for some reason not available from serf

* updated changelog text

* updated dependent testcases

* multiselection version filter

* Update agent/consul/state/catalog.go

comments updated

Co-authored-by: Jared Kirschner <85913323+jkirschner-hashicorp@users.noreply.github.com>

---------

Co-authored-by: Jared Kirschner <85913323+jkirschner-hashicorp@users.noreply.github.com>
  • Loading branch information
vijayraghav-io and jkirschner-hashicorp authored Jul 12, 2023
1 parent f51a9d2 commit 2f20c77
Show file tree
Hide file tree
Showing 26 changed files with 397 additions and 16 deletions.
3 changes: 3 additions & 0 deletions .changelog/17754.txt
Original file line number Diff line number Diff line change
@@ -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.
```
1 change: 1 addition & 0 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
3 changes: 2 additions & 1 deletion agent/agent_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions agent/consul/leader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions agent/consul/state/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
25 changes: 22 additions & 3 deletions agent/consul/state/catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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)
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand All @@ -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])
Expand Down
31 changes: 31 additions & 0 deletions agent/consul/state/state_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions agent/local/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions agent/structs/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}}
Expand Down
96 changes: 96 additions & 0 deletions agent/ui_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -110,32 +113,104 @@ 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)
}
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)
}
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) {
Expand Down Expand Up @@ -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]
Expand All @@ -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
}

Expand Down
6 changes: 6 additions & 0 deletions agent/ui_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions api/catalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 2f20c77

Please sign in to comment.