From ea93c7b29c13595cd389c246f3ba6cf16b99c1d6 Mon Sep 17 00:00:00 2001 From: hc-github-team-consul-core Date: Mon, 17 Jul 2023 12:27:50 -0500 Subject: [PATCH] Backport of Displays Consul version of each nodes in UI nodes section into release/1.16.x (#18113) ## Backport This PR is auto-generated from #17754 to be assessed for backporting due to the inclusion of the label backport/1.16. :rotating_light: >**Warning** automatic cherry-pick of commits failed. If the first commit failed, you will see a blank no-op commit below. If at least one commit succeeded, you will see the cherry-picked commits up to, _not including_, the commit where the merge conflict occurred. The person who merged in the original PR is: @WenInCode This person should manually cherry-pick the original PR into a new backport PR, and close this one when the manual backport PR is merged in. > merge conflict error: unable to process merge commit: "1c757b8a2c1160ad53421b7b8bd7f74b205c4b89", automatic backport requires rebase workflow The below text is copied from the body of the original PR. --- fixes #17097 Consul version of each nodes in UI nodes section @jkirschner-hashicorp @huikang @team @Maintainers Updated consul version in the request to register consul. Added this as Node MetaData. Fetching this new metadata in UI Screenshot 2023-06-15 at 4 21 33 PM Also made this backward compatible and tested. Backward compatible in this context means - If consul binary with above PR changes is deployed to one of node, and if UI is run from this node, then the version of not only current (upgraded) node is displayed in UI , but also of older nodes given that they are consul servers only. For older (non-server or client) nodes the version is not added in NodeMeta Data and hence the version will not be displayed for them. If a old node is consul server, the version will be displayed. As the endpoint - "v1/internal/ui/nodes?dc=dc1" was already returning version in service meta. This is made use of in current UI changes. Screenshot 2023-06-16 at 6 58 32 PM ---
Overview of commits - 931fdfc7ecdc26bb7cc20b698c5e14c1b65fcc6e - b3e2ec1ccaca3832a088ffcac54257fa6653c6c1 - 8d0e9a54907039c09330c6cd7b9e761566af6856 - 04e5d88cca37821f6667be381c16aaa5958b5c92 - 28286a2e98f8cd66ef8593c2e2893b4db6080417 - 43e50ad38207952a9c4d04d45d08b6b8f71b31fe - 0cf1b7077cdf255596254d9dc1624a269c42b94d - 27f34ce1c2973591f75b1e38a81ccbe7cee6cee3 - 2ac76d62b8cbae76b1a903021aebb9b865e29d6e - 3d618df9ef1d10dd5056c8b1ed865839c553a0e0 - 1c757b8a2c1160ad53421b7b8bd7f74b205c4b89 - 23ce82b4cee8f74dd634dbe145313e9a56c0077d - 4dc1c9b4c5aafdb8883ef977dfa9b39da138b6cb - 85a12a92528bfa267a039a9bb258170be914abf7 - 25d30a3fa980d130a30d445d26d47ef2356cb553 - 7f1d6192dce3352e92307175848b89f91e728c24 - 5174cbff84b0795d4cb36eb8980d0d5336091ac9
--------- Co-authored-by: Vijay Srinivas Co-authored-by: John Murret Co-authored-by: Jared Kirschner <85913323+jkirschner-hashicorp@users.noreply.github.com> --- .changelog/17754.txt | 3 + agent/agent.go | 1 + agent/agent_endpoint_test.go | 3 +- agent/consul/leader.go | 10 + agent/consul/state/catalog.go | 7 + agent/consul/state/catalog_test.go | 25 +- agent/consul/state/state_store_test.go | 31 +++ agent/local/state_test.go | 12 +- agent/structs/structs.go | 3 + agent/ui_endpoint.go | 96 +++++++ agent/ui_endpoint_test.go | 6 + api/catalog_test.go | 1 + api/txn_test.go | 5 +- sdk/testutil/server.go | 2 + .../app/components/consul/node/list/index.hbs | 12 + .../consul/node/search-bar/index.hbs | 241 +++++++++--------- .../consul-ui/app/filter/predicates/node.js | 1 + ui/packages/consul-ui/app/models/node.js | 5 + .../consul-ui/app/serializers/application.js | 45 ++++ .../consul-ui/app/sort/comparators/node.js | 37 +++ .../app/templates/dc/nodes/index.hbs | 8 +- .../consul-ui/mock-api/v1/internal/ui/node/_ | 1 + .../consul-ui/mock-api/v1/internal/ui/nodes | 1 + .../tests/unit/sort/comparators/node-test.js | 45 ++++ .../consul-ui/translations/common/en-us.yaml | 4 + .../consul-ui/vendor/consul-ui/routes.js | 1 + 26 files changed, 477 insertions(+), 129 deletions(-) create mode 100644 .changelog/17754.txt create mode 100644 ui/packages/consul-ui/tests/unit/sort/comparators/node-test.js diff --git a/.changelog/17754.txt b/.changelog/17754.txt new file mode 100644 index 000000000..56ab20dc2 --- /dev/null +++ b/.changelog/17754.txt @@ -0,0 +1,3 @@ +```release-note:feature +ui: consul version is displayed in nodes list with filtering and sorting based on versions +``` diff --git a/agent/agent.go b/agent/agent.go index fa75a1cd1..881b94209 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 9f4210ac8..0a37ae174 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -1508,7 +1508,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 c91655c5c..4bc1908d5 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 74efc3229..4e9fcf716 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 0de535c3b..e6b279580 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 fef750253..587f15c03 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 0a78f321f..4751352ec 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 f56dd8f6a..fc9783fa8 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 3de9eac15..8f5184969 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 5fc2e06d3..f6810db80 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 622669135..2b0a4097b 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 975f3e381..ea454976d 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 d00850d5e..a20f95123 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 c1392bd68..f57d7d5ab 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}} +
+
+ ConsulVersion +
+
+ {{!-- Displaying consul version from node meta data --}} + {{#if item.Meta.consul-version}} + + v{{item.Meta.consul-version}} + {{/if}} +
+
diff --git a/ui/packages/consul-ui/app/components/consul/node/search-bar/index.hbs b/ui/packages/consul-ui/app/components/consul/node/search-bar/index.hbs index 3aa868181..e22e6b2b0 100644 --- a/ui/packages/consul-ui/app/components/consul/node/search-bar/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/node/search-bar/index.hbs @@ -1,136 +1,139 @@ {{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: MPL-2.0 +Copyright (c) HashiCorp, Inc. +SPDX-License-Identifier: MPL-2.0 }} - - <:status as |search|> + + <:status as |search|> -{{#let + {{#let - (t (concat "components.consul.node.search-bar." search.status.key) - default=(array - (concat "common.search." search.status.key) - (concat "common.consul." search.status.key) - ) - ) + (t (concat "components.consul.node.search-bar." search.status.key) + default=(array + (concat "common.search." search.status.key) + (concat "common.consul." search.status.key) + ) + ) - (t (concat "components.consul.node.search-bar." search.status.value) - default=(array - (concat "common.search." search.status.value) - (concat "common.consul." search.status.value) - (concat "common.brand." search.status.value) - ) - ) + (if search.status.value + search.status.value + (t (concat "components.consul.node.search-bar." search.status.value) + default=(array + (concat "common.search." search.status.value) + (concat "common.consul." search.status.value) + (concat "common.brand." search.status.value) + )) + ) -as |key value|}} - -
-
{{key}}
-
{{value}}
-
-
-{{/let}} - - - <:search as |search|> - - - - - {{t "common.search.searchproperty"}} - - - - {{#let components.Optgroup components.Option as |Optgroup Option|}} - {{#each @filter.searchproperty.default as |prop|}} - - {{/each}} + as |key value|}} + +
+
{{key}}
+
{{value}}
+
+
{{/let}} -
-
-
- - <:filter as |search|> - + + + <:search as |search|> + + - {{t "common.consul.status"}} + {{t "common.search.searchproperty"}} - {{#let components.Optgroup components.Option as |Optgroup Option|}} - {{#each (array "passing" "warning" "critical") as |state|}} - - {{/each}} - {{/let}} + {{/each}} + {{/let}} - - <:sort as |search|> - + + <:filter as |search|> + + + + {{t "common.consul.status"}} + + + + {{#let components.Optgroup components.Option as |Optgroup Option|}} + {{#each (array "passing" "warning" "critical") as |state|}} + + {{/each}} + {{/let}} + + + - - - {{#let (from-entries (array - (array "Node:asc" (t "common.sort.alpha.asc")) - (array "Node:desc" (t "common.sort.alpha.desc")) - (array "Status:asc" (t "common.sort.status.asc")) - (array "Status:desc" (t "common.sort.status.desc")) - )) - as |selectable| - }} - {{get selectable @sort.value}} - {{/let}} - - - - {{#let components.Optgroup components.Option as |Optgroup Option|}} - - - - - - - - - {{/let}} - - - -
\ No newline at end of file + + + {{t "common.consul.version"}} + + + + {{#let components.Optgroup components.Option as |Optgroup Option|}} + {{#each @versions as |version|}} + + {{/each}} + {{/let}} + + + + <:sort as |search|> + + + + {{#let (from-entries (array + (array "Node:asc" (t "common.sort.alpha.asc")) + (array "Node:desc" (t "common.sort.alpha.desc")) + (array "Status:asc" (t "common.sort.status.asc")) + (array "Status:desc" (t "common.sort.status.desc")) + (array "Version:asc" (t "common.sort.version.asc")) + (array "Version:desc" (t "common.sort.version.desc")) + )) + as |selectable| + }} + {{get selectable @sort.value}} + {{/let}} + + + + {{#let components.Optgroup components.Option as |Optgroup Option|}} + + + + + + + + + + + + + {{/let}} + + + +
diff --git a/ui/packages/consul-ui/app/filter/predicates/node.js b/ui/packages/consul-ui/app/filter/predicates/node.js index b5c752ab9..83eb3c98e 100644 --- a/ui/packages/consul-ui/app/filter/predicates/node.js +++ b/ui/packages/consul-ui/app/filter/predicates/node.js @@ -9,4 +9,5 @@ export default { warning: (item, value) => item.Status === value, critical: (item, value) => item.Status === value, }, + version: (item, value) => item.Version.includes(value + '.'), }; diff --git a/ui/packages/consul-ui/app/models/node.js b/ui/packages/consul-ui/app/models/node.js index e56f3e1ff..b95ec2830 100644 --- a/ui/packages/consul-ui/app/models/node.js +++ b/ui/packages/consul-ui/app/models/node.js @@ -67,4 +67,9 @@ export default class Node extends Model { get ChecksWarning() { return this.NodeChecks.filter((item) => item.Status === 'warning').length; } + + @computed('Meta') + get Version() { + return this.Meta?.['consul-version'] ?? ''; + } } diff --git a/ui/packages/consul-ui/app/serializers/application.js b/ui/packages/consul-ui/app/serializers/application.js index 2e6bac137..d9e58b0ab 100644 --- a/ui/packages/consul-ui/app/serializers/application.js +++ b/ui/packages/consul-ui/app/serializers/application.js @@ -156,6 +156,10 @@ export default class ApplicationSerializer extends Serializer { // ember-data methods so we have the opportunity to do this on a per-model // level const meta = this.normalizeMeta(store, modelClass, normalizedPayload, id, requestType); + // get distinct consul versions from list and add it as meta + if (modelClass.modelName === 'node' && requestType === 'query') { + meta.versions = this.getDistinctConsulVersions(normalizedPayload); + } if (requestType !== 'query') { normalizedPayload.meta = meta; } @@ -215,4 +219,45 @@ export default class ApplicationSerializer extends Serializer { normalizePayload(payload, id, requestType) { return payload; } + + // getDistinctConsulVersions will be called only for nodes and query request type + // the list of versions is to be added as meta to resp, without changing original response structure + // hence this function is added in application.js + getDistinctConsulVersions(payload) { + // create a Set and add version with only major.minor : ex-1.24.6 as 1.24 + let versionSet = new Set(); + payload.forEach(function (item) { + if (item.Meta && item.Meta['consul-version'] !== '') { + const split = item.Meta['consul-version'].split('.'); + versionSet.add(split[0] + '.' + split[1]); + } + }); + + const versionArray = Array.from(versionSet); + + // Sort the array in descending order using a custom comparison function + versionArray.sort((a, b) => { + // Split the versions into arrays of numbers + const versionA = a.split('.').map((part) => { + const number = Number(part); + return isNaN(number) ? 0 : number; + }); + const versionB = b.split('.').map((part) => { + const number = Number(part); + return isNaN(number) ? 0 : number; + }); + + const minLength = Math.min(versionA.length, versionB.length); + + // start with comparing major version num, if equal then compare minor + for (let i = 0; i < minLength; i++) { + if (versionA[i] !== versionB[i]) { + return versionB[i] - versionA[i]; + } + } + return versionB.length - versionA.length; + }); + + return versionArray; //sorted array + } } diff --git a/ui/packages/consul-ui/app/sort/comparators/node.js b/ui/packages/consul-ui/app/sort/comparators/node.js index c584456e3..fb7055075 100644 --- a/ui/packages/consul-ui/app/sort/comparators/node.js +++ b/ui/packages/consul-ui/app/sort/comparators/node.js @@ -38,6 +38,43 @@ export default ({ properties }) => return 0; } }; + } else if (key.startsWith('Version:')) { + return function (itemA, itemB) { + const [, dir] = key.split(':'); + let a, b; + if (dir === 'asc') { + a = itemA; + b = itemB; + } else { + b = itemA; + a = itemB; + } + + // Split the versions into arrays of numbers + const versionA = a.Version.split('.').map((part) => { + const number = Number(part); + return isNaN(number) ? 0 : number; + }); + const versionB = b.Version.split('.').map((part) => { + const number = Number(part); + return isNaN(number) ? 0 : number; + }); + + const minLength = Math.min(versionA.length, versionB.length); + + for (let i = 0; i < minLength; i++) { + const diff = versionA[i] - versionB[i]; + switch (true) { + case diff > 0: + return 1; + case diff < 0: + return -1; + } + } + + return versionA.length - versionB.length; + }; } + return properties(['Node'])(key); }; diff --git a/ui/packages/consul-ui/app/templates/dc/nodes/index.hbs b/ui/packages/consul-ui/app/templates/dc/nodes/index.hbs index 4d0eb2e3a..56f7676c4 100644 --- a/ui/packages/consul-ui/app/templates/dc/nodes/index.hbs +++ b/ui/packages/consul-ui/app/templates/dc/nodes/index.hbs @@ -40,10 +40,15 @@ change=(action (mut searchproperty) value='target.selectedItems') default=this._searchProperties ) + version=(hash + value=(if this.version (split this.version ',') undefined) + change=(action (mut this.version) value='target.selectedItems') + ) ) api.data leader.data - as |sort filters items leader| + api.data.meta.versions + as |sort filters items leader versions| }} {{#let (reject-by 'Meta.synthetic-node' items) as |filtered|}} @@ -61,6 +66,7 @@ @onsearch={{action (mut search) value='target.value'}} @sort={{sort}} @filter={{filters}} + @versions={{versions}} /> {{/if}} diff --git a/ui/packages/consul-ui/mock-api/v1/internal/ui/node/_ b/ui/packages/consul-ui/mock-api/v1/internal/ui/node/_ index 27afd9c8e..2adf163f7 100644 --- a/ui/packages/consul-ui/mock-api/v1/internal/ui/node/_ +++ b/ui/packages/consul-ui/mock-api/v1/internal/ui/node/_ @@ -30,6 +30,7 @@ return ` "TaggedAddresses":{"lan":"${ip}","wan":"${ip}"}, "Meta":{ "consul-network-segment":"", + "consul-version": "${env('CONSUL_VERSION') ? fake.helpers.randomize([env('CONSUL_VERSION'),"1.10.4","1.15.2", "1.17.8","1.7.2","1.12.4", "1.17.2","1.0.9","2.0.2"]) : fake.helpers.randomize(["1.10.4","1.15.2", "1.17.8","1.7.2","1.12.4", "1.17.2","1.0.9","2.0.2"]) }", "consul-dashboard-url": "${fake.internet.protocol()}://${fake.internet.domainName()}/?id={{Node}}" }, "Services":[ diff --git a/ui/packages/consul-ui/mock-api/v1/internal/ui/nodes b/ui/packages/consul-ui/mock-api/v1/internal/ui/nodes index 2740d824a..de64b0bb8 100644 --- a/ui/packages/consul-ui/mock-api/v1/internal/ui/nodes +++ b/ui/packages/consul-ui/mock-api/v1/internal/ui/nodes @@ -25,6 +25,7 @@ }, "Meta": { "consul-network-segment":"", + "consul-version": "${env('CONSUL_VERSION') ? fake.helpers.randomize([env('CONSUL_VERSION'),"1.10.4","1.15.2", "1.17.8","1.7.2","1.12.4", "1.17.2","1.0.9","2.0.2"]) : fake.helpers.randomize(["1.10.4","1.15.2", "1.17.8","1.7.2","1.12.4", "1.17.2","1.0.9","2.0.2"]) }", "synthetic-node": ${env('CONSUL_AGENTLESS_ENABLED') ? fake.helpers.randomize([true, false, false, false]) : false} }, "Services":[ diff --git a/ui/packages/consul-ui/tests/unit/sort/comparators/node-test.js b/ui/packages/consul-ui/tests/unit/sort/comparators/node-test.js new file mode 100644 index 000000000..9b5c5ea75 --- /dev/null +++ b/ui/packages/consul-ui/tests/unit/sort/comparators/node-test.js @@ -0,0 +1,45 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import comparators from 'consul-ui/sort/comparators/node'; +import { properties } from 'consul-ui/services/sort'; +import { module, test } from 'qunit'; + +module('Unit | Sort | Comparator | node', function () { + const comparator = comparators({ properties }); + test('items are sorted by a fake Version', function (assert) { + const items = [ + { + Version: '2.24.1', + }, + { + Version: '1.12.6', + }, + { + Version: '2.09.3', + }, + ]; + const comp = comparator('Version:asc'); + assert.equal(typeof comp, 'function'); + + const expected = [ + { + Version: '1.12.6', + }, + { + Version: '2.09.3', + }, + { + Version: '2.24.1', + }, + ]; + let actual = items.sort(comp); + assert.deepEqual(actual, expected); + + expected.reverse(); + actual = items.sort(comparator('Version:desc')); + assert.deepEqual(actual, expected); + }); +}); diff --git a/ui/packages/consul-ui/translations/common/en-us.yaml b/ui/packages/consul-ui/translations/common/en-us.yaml index fc7f542fe..b0dc3bffe 100644 --- a/ui/packages/consul-ui/translations/common/en-us.yaml +++ b/ui/packages/consul-ui/translations/common/en-us.yaml @@ -53,6 +53,7 @@ consul: redundancyzone: Redundancy zone peername: Peer partition: Admin Partitions + version: Version search: search: Search searchproperty: Search Across @@ -77,6 +78,9 @@ sort: status: asc: Unhealthy to Healthy desc: Healthy to Unhealthy + version: + asc: Oldest to Latest + desc: Latest to Oldest validations: dns-hostname: help: | diff --git a/ui/packages/consul-ui/vendor/consul-ui/routes.js b/ui/packages/consul-ui/vendor/consul-ui/routes.js index 79aadcd6a..ec25fb0a2 100644 --- a/ui/packages/consul-ui/vendor/consul-ui/routes.js +++ b/ui/packages/consul-ui/vendor/consul-ui/routes.js @@ -218,6 +218,7 @@ queryParams: { sortBy: 'sort', status: 'status', + version: 'version', searchproperty: { as: 'searchproperty', empty: [['Node', 'Address', 'Meta', 'PeerName']],