mirror of https://github.com/status-im/consul.git
Displays Consul version of each nodes in UI nodes section (#17754)
* 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>
This commit is contained in:
parent
f51a9d29ae
commit
2f20c77e4d
|
@ -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.
|
||||
```
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: {}}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -50,5 +50,17 @@ as |item index|>
|
|||
{{item.Address}}
|
||||
</dd>
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>
|
||||
<span>ConsulVersion</span>
|
||||
</dt>
|
||||
<dd>
|
||||
{{!-- Displaying consul version from node meta data --}}
|
||||
{{#if item.Meta.consul-version}}
|
||||
<FlightIcon class='w-4 h-4' @size='24' @name='consul-color' @stretched={{true}} />
|
||||
<span>v{{item.Meta.consul-version}}</span>
|
||||
{{/if}}
|
||||
</dd>
|
||||
</dl>
|
||||
</BlockSlot>
|
||||
</ListCollection>
|
||||
|
|
|
@ -19,12 +19,14 @@
|
|||
)
|
||||
)
|
||||
|
||||
(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|}}
|
||||
|
@ -95,6 +97,27 @@ as |key value|}}
|
|||
{{/let}}
|
||||
</BlockSlot>
|
||||
</search.Select>
|
||||
<search.Select
|
||||
class="type-version"
|
||||
@position="left"
|
||||
@onchange={{action @filter.version.change}}
|
||||
@multiple={{true}}
|
||||
as |components|>
|
||||
<BlockSlot @name="selected">
|
||||
<span>
|
||||
{{t "common.consul.version"}}
|
||||
</span>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="options">
|
||||
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
||||
{{#each @versions as |version|}}
|
||||
<Option @value={{version}} @selected={{includes version @filter.version.value}}>
|
||||
{{concat version ".x" }}
|
||||
</Option>
|
||||
{{/each}}
|
||||
{{/let}}
|
||||
</BlockSlot>
|
||||
</search.Select>
|
||||
</:filter>
|
||||
<:sort as |search|>
|
||||
<search.Select
|
||||
|
@ -112,6 +135,8 @@ as |key value|}}
|
|||
(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|
|
||||
}}
|
||||
|
@ -129,6 +154,10 @@ as |key value|}}
|
|||
<Option @value="Node:asc" @selected={{eq "Node:asc" @sort.value}}>{{t "common.sort.alpha.asc"}}</Option>
|
||||
<Option @value="Node:desc" @selected={{eq "Node:desc" @sort.value}}>{{t "common.sort.alpha.desc"}}</Option>
|
||||
</Optgroup>
|
||||
<Optgroup @label={{t "common.consul.version"}}>
|
||||
<Option @value="Version:asc" @selected={{eq "Version:asc" @sort.value}}>{{t "common.sort.version.asc"}}</Option>
|
||||
<Option @value="Version:desc" @selected={{eq "Version:desc" @sort.value}}>{{t "common.sort.version.desc"}}</Option>
|
||||
</Optgroup>
|
||||
{{/let}}
|
||||
</BlockSlot>
|
||||
</search.Select>
|
||||
|
|
|
@ -9,4 +9,12 @@ export default {
|
|||
warning: (item, value) => item.Status === value,
|
||||
critical: (item, value) => item.Status === value,
|
||||
},
|
||||
version: (item, value) => {
|
||||
for (const element of value) {
|
||||
if (item.Version.includes(element + '.')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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'] ?? '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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|}}
|
||||
<AppView>
|
||||
|
@ -61,6 +66,7 @@
|
|||
@onsearch={{action (mut search) value='target.value'}}
|
||||
@sort={{sort}}
|
||||
@filter={{filters}}
|
||||
@versions={{versions}}
|
||||
/>
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
|
|
|
@ -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":[
|
||||
|
|
|
@ -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":[
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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: |
|
||||
|
|
|
@ -218,6 +218,7 @@
|
|||
queryParams: {
|
||||
sortBy: 'sort',
|
||||
status: 'status',
|
||||
version: 'version',
|
||||
searchproperty: {
|
||||
as: 'searchproperty',
|
||||
empty: [['Node', 'Address', 'Meta', 'PeerName']],
|
||||
|
|
Loading…
Reference in New Issue