Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fingerprint: update consul fingerprinter with additional attributes #10699

Merged
merged 5 commits into from
Jun 3, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
client/fingerprint/consul: refactor the consul fingerprinter to test …
…individual attributes

This PR refactors the ConsulFingerprint implementation, breaking individual attributes
into individual functions to make testing them easier. This is in preparation for
additional extractors about to be added. Behavior should be otherwise unchanged.

It adds the attribute consul.sku, which can be used to differentiate between Consul
OSS vs Consul ENT.
  • Loading branch information
shoenig committed Jun 3, 2021
commit 0479167252d955b78f289d65c830cbf94484851b
191 changes: 130 additions & 61 deletions client/fingerprint/consul.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package fingerprint
import (
"fmt"
"strconv"
"strings"
"time"

consul "github.com/hashicorp/consul/api"
log "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-version"
)

const (
Expand All @@ -16,108 +18,175 @@ const (

// ConsulFingerprint is used to fingerprint for Consul
type ConsulFingerprint struct {
logger log.Logger
client *consul.Client
lastState string
logger log.Logger
client *consul.Client
lastState string
extractors map[string]consulExtractor
}

// consulInfo aliases the type returned from the Consul agent self endpoint.
type consulInfo = map[string]map[string]interface{}

// consulExtractor is used to parse out one attribute from consulInfo. Returns
// the value of the attribute, and whether the attribute exists.
type consulExtractor func(consulInfo) (string, bool)

// NewConsulFingerprint is used to create a Consul fingerprint
func NewConsulFingerprint(logger log.Logger) Fingerprint {
return &ConsulFingerprint{logger: logger.Named("consul"), lastState: consulUnavailable}
return &ConsulFingerprint{
logger: logger.Named("consul"),
lastState: consulUnavailable,
}
}

func (f *ConsulFingerprint) Fingerprint(req *FingerprintRequest, resp *FingerprintResponse) error {
// Only create the client once to avoid creating too many connections to
// Consul.

// establish consul client if necessary
if err := f.initialize(req); err != nil {
return err
}

// query consul for agent self api
info := f.query(resp)
if len(info) == 0 {
// unable to reach consul, nothing to do this time
return nil
}

// apply the extractor for each attribute
for attr, extractor := range f.extractors {
if s, ok := extractor(info); !ok {
f.logger.Warn("unable to fingerprint consul", "attribute", attr)
} else {
resp.AddAttribute(attr, s)
}
}

// create link for consul
f.link(resp)

// indicate Consul is now available
if f.lastState == consulUnavailable {
f.logger.Info("consul agent is available")
}

f.lastState = consulAvailable
resp.Detected = true
return nil
}

func (f *ConsulFingerprint) Periodic() (bool, time.Duration) {
return true, 15 * time.Second
}

// clearConsulAttributes removes consul attributes and links from the passed Node.
func (f *ConsulFingerprint) clearConsulAttributes(r *FingerprintResponse) {
for attr := range f.extractors {
r.RemoveAttribute(attr)
}
r.RemoveLink("consul")
}

func (f *ConsulFingerprint) initialize(req *FingerprintRequest) error {
// Only create the Consul client once to avoid creating many connections
if f.client == nil {
consulConfig, err := req.Config.ConsulConfig.ApiConfig()
if err != nil {
return fmt.Errorf("Failed to initialize the Consul client config: %v", err)
return fmt.Errorf("failed to initialize Consul client config: %v", err)
}

f.client, err = consul.NewClient(consulConfig)
if err != nil {
return fmt.Errorf("Failed to initialize consul client: %s", err)
return fmt.Errorf("failed to initialize Consul client: %s", err)
}

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,
}
}

return nil
}

func (f *ConsulFingerprint) query(resp *FingerprintResponse) consulInfo {
// We'll try to detect consul by making a query to to the agent's self API.
// If we can't hit this URL consul is probably not running on this machine.
info, err := f.client.Agent().Self()
if err != nil {
f.clearConsulAttributes(resp)

// Print a message indicating that the Consul Agent is not available
// anymore
// indicate consul no longer available
if f.lastState == consulAvailable {
f.logger.Info("consul agent is unavailable")
}
f.lastState = consulUnavailable
return nil
}
return info
}

if s, ok := info["Config"]["Server"].(bool); ok {
resp.AddAttribute("consul.server", strconv.FormatBool(s))
} else {
f.logger.Warn("unable to fingerprint consul.server")
}
if v, ok := info["Config"]["Version"].(string); ok {
resp.AddAttribute("consul.version", v)
} else {
f.logger.Warn("unable to fingerprint consul.version")
}
if r, ok := info["Config"]["Revision"].(string); ok {
resp.AddAttribute("consul.revision", r)
} else {
f.logger.Warn("unable to fingerprint consul.revision")
}
if n, ok := info["Config"]["NodeName"].(string); ok {
resp.AddAttribute("unique.consul.name", n)
} else {
f.logger.Warn("unable to fingerprint unique.consul.name")
}
if d, ok := info["Config"]["Datacenter"].(string); ok {
resp.AddAttribute("consul.datacenter", d)
} else {
f.logger.Warn("unable to fingerprint consul.datacenter")
}
if g, ok := info["Member"]["Tags"].(map[string]interface{}); ok {
if s, ok := g["segment"].(string); ok {
resp.AddAttribute("consul.segment", s)
}
} else {
f.logger.Warn("unable to fingerprint consul.segment")
}
func (f *ConsulFingerprint) link(resp *FingerprintResponse) {
if dc, ok := resp.Attributes["consul.datacenter"]; ok {
if name, ok2 := resp.Attributes["unique.consul.name"]; ok2 {
resp.AddLink("consul", fmt.Sprintf("%s.%s", dc, name))
}
} else {
f.logger.Warn("malformed Consul response prevented linking")
}
}

// If the Consul Agent was previously unavailable print a message to
// indicate the Agent is available now
if f.lastState == consulUnavailable {
f.logger.Info("consul agent is available")
func (f *ConsulFingerprint) server(info consulInfo) (string, bool) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love how legible this entire ConsulFingerprint becomes with these extractor functions.

This refactoring does make me realize that if Consul were to push a backwards incompatible JSON we could potentially panic Nomad when we do the type cast. Probably something for future robustness work to worry about rather than this PR though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're actually okay (for panics) on type changes - each of these type casts is in the context of a type check which will simply become false. The attributes will end up being removed which isn't great, of course, and I'll probably have a few choice words with Consul team 😅

s, ok := info["Config"]["Server"].(bool)
return strconv.FormatBool(s), ok
}

func (f *ConsulFingerprint) version(info consulInfo) (string, bool) {
v, ok := info["Config"]["Version"].(string)
return v, ok
}

func (f *ConsulFingerprint) sku(info consulInfo) (string, bool) {
v, ok := info["Config"]["Version"].(string)
if !ok {
return "", ok
}
f.lastState = consulAvailable
resp.Detected = true
return nil

ver, vErr := version.NewVersion(v)
if vErr != nil {
return "", false
}
if strings.Contains(ver.Metadata(), "ent") {
return "ent", true
}
return "oss", true
}

// clearConsulAttributes removes consul attributes and links from the passed
// Node.
func (f *ConsulFingerprint) clearConsulAttributes(r *FingerprintResponse) {
r.RemoveAttribute("consul.server")
r.RemoveAttribute("consul.version")
r.RemoveAttribute("consul.revision")
r.RemoveAttribute("unique.consul.name")
r.RemoveAttribute("consul.datacenter")
r.RemoveAttribute("consul.segment")
r.RemoveLink("consul")
func (f *ConsulFingerprint) revision(info consulInfo) (string, bool) {
r, ok := info["Config"]["Revision"].(string)
return r, ok
}

func (f *ConsulFingerprint) Periodic() (bool, time.Duration) {
return true, 15 * time.Second
func (f *ConsulFingerprint) name(info consulInfo) (string, bool) {
n, ok := info["Config"]["NodeName"].(string)
return n, ok
}

func (f *ConsulFingerprint) dc(info consulInfo) (string, bool) {
d, ok := info["Config"]["Datacenter"].(string)
return d, ok
}

func (f *ConsulFingerprint) segment(info consulInfo) (string, bool) {
tags, tagsOK := info["Member"]["Tags"].(map[string]interface{})
if !tagsOK {
return "", false
}
s, ok := tags["segment"].(string)
return s, ok
}
Loading