Skip to content

Commit

Permalink
client/fingerprint/consul: add new attributes to consul fingerprinter
Browse files Browse the repository at this point in the history
This PR adds new probes for detecting these new Consul related attributes:

Consul namespaces are a Consul enterprise feature that may be disabled depending
on the enterprise license associated with the Consul servers. Having this attribute
available will enable Nomad to properly decide whether to query the Consul Namespace
API.

Consul connect must be explicitly enabled before Connect APIs will work. Currently
Nomad only checks for a minimum Consul version. Having this attribute available will
enable Nomad to properly schedule Connect tasks only on nodes with a Consul agent that
has Connect enabled.

Consul connect requires the grpc port to be explicitly set before Connect APIs will work.
Currently Nomad only checks for a minimal Consul version. Having this attribute available
will enable Nomad to schedule Connect tasks only on nodes with a Consul agent that has
the grpc listener enabled.
  • Loading branch information
shoenig committed Jun 3, 2021
1 parent 0479167 commit b3254f6
Show file tree
Hide file tree
Showing 4 changed files with 749 additions and 16 deletions.
53 changes: 46 additions & 7 deletions client/fingerprint/consul.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,16 @@ func (f *ConsulFingerprint) initialize(req *FingerprintRequest) error {
}

f.extractors = map[string]consulExtractor{
"consul.server": f.server,
"consul.version": f.version,
"consul.sku": f.sku,
"consul.revision": f.revision,
"unique.consul.name": f.name,
"consul.datacenter": f.dc,
"consul.segment": f.segment,
"consul.server": f.server,
"consul.version": f.version,
"consul.sku": f.sku,
"consul.revision": f.revision,
"unique.consul.name": f.name,
"consul.datacenter": f.dc,
"consul.segment": f.segment,
"consul.connect": f.connect,
"consul.grpc": f.grpc,
"consul.ft.namespaces": f.namespaces,
}
}

Expand Down Expand Up @@ -190,3 +193,39 @@ func (f *ConsulFingerprint) segment(info consulInfo) (string, bool) {
s, ok := tags["segment"].(string)
return s, ok
}

func (f *ConsulFingerprint) connect(info consulInfo) (string, bool) {
c, ok := info["DebugConfig"]["ConnectEnabled"].(bool)
return strconv.FormatBool(c), ok
}

func (f *ConsulFingerprint) grpc(info consulInfo) (string, bool) {
p, ok := info["DebugConfig"]["GRPCPort"].(float64)
return fmt.Sprintf("%d", int(p)), ok
}

func (f *ConsulFingerprint) namespaces(info consulInfo) (string, bool) {
return f.feature("Namespaces", info)
}

// possible values as of v1.9.5+ent:
// Automated Backups, Automated Upgrades, Enhanced Read Scalability,
// Network Segments, Redundancy Zone, Advanced Network Federation,
// Namespaces, SSO, Audit Logging
func (f *ConsulFingerprint) feature(name string, info consulInfo) (string, bool) {
lic, licOK := info["Stats"]["license"].(map[string]interface{})
if !licOK {
return "", false
}

features, exists := lic["features"].(string)
if !exists {
return "", false
}

if !strings.Contains(features, name) {
return "", false
}

return "true", true
}
210 changes: 201 additions & 9 deletions client/fingerprint/consul_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,105 @@ func TestConsulFingerprint_segment(t *testing.T) {
})
}

func TestConsulFingerprint_Fingerprint(t *testing.T) {
func TestConsulFingerprint_connect(t *testing.T) {
t.Parallel()

fp := newConsulFingerPrint(t)

t.Run("connect enabled", func(t *testing.T) {
s, ok := fp.connect(consulInfo{
"DebugConfig": {"ConnectEnabled": true},
})
require.True(t, ok)
require.Equal(t, "true", s)
})

t.Run("connect not enabled", func(t *testing.T) {
s, ok := fp.connect(consulInfo{
"DebugConfig": {"ConnectEnabled": false},
})
require.True(t, ok)
require.Equal(t, "false", s)
})

t.Run("connect missing", func(t *testing.T) {
_, ok := fp.connect(consulInfo{
"DebugConfig": {},
})
require.False(t, ok)
})
}

func TestConsulFingerprint_grpc(t *testing.T) {
t.Parallel()

fp := newConsulFingerPrint(t)

t.Run("grpc set", func(t *testing.T) {
s, ok := fp.grpc(consulInfo{
"DebugConfig": {"GRPCPort": 8502.0}, // JSON numbers are floats
})
require.True(t, ok)
require.Equal(t, "8502", s)
})

t.Run("grpc disabled", func(t *testing.T) {
s, ok := fp.grpc(consulInfo{
"DebugConfig": {"GRPCPort": -1.0}, // JSON numbers are floats
})
require.True(t, ok)
require.Equal(t, "-1", s)
})

t.Run("grpc missing", func(t *testing.T) {
_, ok := fp.grpc(consulInfo{
"DebugConfig": {},
})
require.False(t, ok)
})

}

func TestConsulFingerprint_namespaces(t *testing.T) {
t.Parallel()

fp := newConsulFingerPrint(t)

t.Run("supports namespaces", func(t *testing.T) {
s, ok := fp.namespaces(consulInfo{
"Stats": {"license": map[string]interface{}{"features": "Automated Backups, Automated Upgrades, Enhanced Read Scalability, Network Segments, Redundancy Zone, Advanced Network Federation, Namespaces, SSO, Audit Logging"}},
})
require.True(t, ok)
require.Equal(t, "true", s)
})

t.Run("no namespaces", func(t *testing.T) {
_, ok := fp.namespaces(consulInfo{
"Stats": {"license": map[string]interface{}{"features": "Automated Backups, Automated Upgrades, Enhanced Read Scalability, Network Segments, Redundancy Zone, Advanced Network Federation, SSO, Audit Logging"}},
})
require.False(t, ok)
})

t.Run("stats missing", func(t *testing.T) {
_, ok := fp.namespaces(consulInfo{})
require.False(t, ok)
})

t.Run("license missing", func(t *testing.T) {
_, ok := fp.namespaces(consulInfo{"Stats": {}})
require.False(t, ok)
})

t.Run("features missing", func(t *testing.T) {
_, ok := fp.namespaces(consulInfo{"Stats": {"license": map[string]interface{}{}}})
require.False(t, ok)
})
}

func TestConsulFingerprint_Fingerprint_oss(t *testing.T) {
cf := newConsulFingerPrint(t)

ts, cfg := fakeConsul(fakeConsulPayload(t, "test_fixtures/consul/agent_self.json"))
ts, cfg := fakeConsul(fakeConsulPayload(t, "test_fixtures/consul/agent_self_oss.json"))
defer ts.Close()

node := &structs.Node{Attributes: make(map[string]string)}
Expand All @@ -282,6 +377,8 @@ func TestConsulFingerprint_Fingerprint(t *testing.T) {
"consul.server": "true",
"consul.sku": "oss",
"consul.version": "1.9.5",
"consul.connect": "true",
"consul.grpc": "8502",
"unique.consul.name": "HAL9000",
}, resp.Attributes)
require.True(t, resp.Detected)
Expand All @@ -298,19 +395,24 @@ func TestConsulFingerprint_Fingerprint(t *testing.T) {
node.Attributes["consul.server"] = "foo"
node.Attributes["consul.sku"] = "foo"
node.Attributes["consul.version"] = "foo"
node.Attributes["consul.connect"] = "foo"
node.Attributes["connect.grpc"] = "foo"
node.Attributes["unique.consul.name"] = "foo"

// execute second query with error
err2 := cf.Fingerprint(&FingerprintRequest{Config: cfg, Node: node}, &resp2)
require.NoError(t, err2) // does not return error
require.Equal(t, map[string]string{ // attributes set empty
"consul.datacenter": "",
"consul.revision": "",
"consul.segment": "",
"consul.server": "",
"consul.sku": "",
"consul.version": "",
"unique.consul.name": "",
"consul.datacenter": "",
"consul.revision": "",
"consul.segment": "",
"consul.server": "",
"consul.sku": "",
"consul.version": "",
"unique.consul.name": "",
"consul.connect": "",
"consul.grpc": "",
"consul.ft.namespaces": "",
}, resp2.Attributes)
require.True(t, resp.Detected) // never downgrade

Expand All @@ -328,10 +430,100 @@ func TestConsulFingerprint_Fingerprint(t *testing.T) {
"consul.server": "true",
"consul.sku": "oss",
"consul.version": "1.9.5",
"consul.connect": "true",
"consul.grpc": "8502",
"unique.consul.name": "HAL9000",
}, resp3.Attributes)

// consul now available again
require.Equal(t, consulAvailable, cf.lastState)
require.True(t, resp.Detected)
}

func TestConsulFingerprint_Fingerprint_ent(t *testing.T) {
cf := newConsulFingerPrint(t)

ts, cfg := fakeConsul(fakeConsulPayload(t, "test_fixtures/consul/agent_self_ent.json"))
defer ts.Close()

node := &structs.Node{Attributes: make(map[string]string)}

// consul not available before first run
require.Equal(t, consulUnavailable, cf.lastState)

// execute first query with good response
var resp FingerprintResponse
err := cf.Fingerprint(&FingerprintRequest{Config: cfg, Node: node}, &resp)
require.NoError(t, err)
require.Equal(t, map[string]string{
"consul.datacenter": "dc1",
"consul.revision": "22ce6c6ad",
"consul.segment": "seg1",
"consul.server": "true",
"consul.sku": "ent",
"consul.version": "1.9.5+ent",
"consul.ft.namespaces": "true",
"consul.connect": "true",
"consul.grpc": "8502",
"unique.consul.name": "HAL9000",
}, resp.Attributes)
require.True(t, resp.Detected)

// consul now available
require.Equal(t, consulAvailable, cf.lastState)

var resp2 FingerprintResponse

// pretend attributes set for failing request
node.Attributes["consul.datacenter"] = "foo"
node.Attributes["consul.revision"] = "foo"
node.Attributes["consul.segment"] = "foo"
node.Attributes["consul.server"] = "foo"
node.Attributes["consul.sku"] = "foo"
node.Attributes["consul.version"] = "foo"
node.Attributes["consul.ft.namespaces"] = "foo"
node.Attributes["consul.connect"] = "foo"
node.Attributes["connect.grpc"] = "foo"
node.Attributes["unique.consul.name"] = "foo"

// execute second query with error
err2 := cf.Fingerprint(&FingerprintRequest{Config: cfg, Node: node}, &resp2)
require.NoError(t, err2) // does not return error
require.Equal(t, map[string]string{ // attributes set empty
"consul.datacenter": "",
"consul.revision": "",
"consul.segment": "",
"consul.server": "",
"consul.sku": "",
"consul.version": "",
"consul.ft.namespaces": "",
"consul.connect": "",
"consul.grpc": "",
"unique.consul.name": "",
}, resp2.Attributes)
require.True(t, resp.Detected) // never downgrade

// consul no longer available
require.Equal(t, consulUnavailable, cf.lastState)

// execute third query no error
var resp3 FingerprintResponse
err3 := cf.Fingerprint(&FingerprintRequest{Config: cfg, Node: node}, &resp3)
require.NoError(t, err3)
require.Equal(t, map[string]string{
"consul.datacenter": "dc1",
"consul.revision": "22ce6c6ad",
"consul.segment": "seg1",
"consul.server": "true",
"consul.sku": "ent",
"consul.version": "1.9.5+ent",
"consul.ft.namespaces": "true",
"consul.connect": "true",
"consul.grpc": "8502",
"unique.consul.name": "HAL9000",
}, resp3.Attributes)

// consul now available again
require.Equal(t, consulAvailable, cf.lastState)
require.True(t, resp.Detected)
}
Loading

0 comments on commit b3254f6

Please sign in to comment.