github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/fingerprint/consul.go (about)

     1  package fingerprint
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  	"strings"
     7  	"time"
     8  
     9  	consulapi "github.com/hashicorp/consul/api"
    10  	log "github.com/hashicorp/go-hclog"
    11  	"github.com/hashicorp/go-version"
    12  	agentconsul "github.com/hashicorp/nomad/command/agent/consul"
    13  )
    14  
    15  const (
    16  	consulAvailable   = "available"
    17  	consulUnavailable = "unavailable"
    18  )
    19  
    20  var (
    21  	// consulGRPCPortChangeVersion is the Consul version which made a breaking
    22  	// change to the way gRPC API listeners are created. This means Nomad must
    23  	// perform different fingerprinting depending on which version of Consul it
    24  	// is communicating with.
    25  	consulGRPCPortChangeVersion = version.Must(version.NewVersion("1.14.0"))
    26  )
    27  
    28  // ConsulFingerprint is used to fingerprint for Consul
    29  type ConsulFingerprint struct {
    30  	logger     log.Logger
    31  	client     *consulapi.Client
    32  	lastState  string
    33  	extractors map[string]consulExtractor
    34  }
    35  
    36  // consulExtractor is used to parse out one attribute from consulInfo. Returns
    37  // the value of the attribute, and whether the attribute exists.
    38  type consulExtractor func(agentconsul.Self) (string, bool)
    39  
    40  // NewConsulFingerprint is used to create a Consul fingerprint
    41  func NewConsulFingerprint(logger log.Logger) Fingerprint {
    42  	return &ConsulFingerprint{
    43  		logger:    logger.Named("consul"),
    44  		lastState: consulUnavailable,
    45  	}
    46  }
    47  
    48  func (f *ConsulFingerprint) Fingerprint(req *FingerprintRequest, resp *FingerprintResponse) error {
    49  
    50  	// establish consul client if necessary
    51  	if err := f.initialize(req); err != nil {
    52  		return err
    53  	}
    54  
    55  	// query consul for agent self api
    56  	info := f.query(resp)
    57  	if len(info) == 0 {
    58  		// unable to reach consul, nothing to do this time
    59  		return nil
    60  	}
    61  
    62  	// apply the extractor for each attribute
    63  	for attr, extractor := range f.extractors {
    64  		if s, ok := extractor(info); !ok {
    65  			f.logger.Warn("unable to fingerprint consul", "attribute", attr)
    66  		} else {
    67  			resp.AddAttribute(attr, s)
    68  		}
    69  	}
    70  
    71  	// create link for consul
    72  	f.link(resp)
    73  
    74  	// indicate Consul is now available
    75  	if f.lastState == consulUnavailable {
    76  		f.logger.Info("consul agent is available")
    77  	}
    78  
    79  	f.lastState = consulAvailable
    80  	resp.Detected = true
    81  	return nil
    82  }
    83  
    84  func (f *ConsulFingerprint) Periodic() (bool, time.Duration) {
    85  	return true, 15 * time.Second
    86  }
    87  
    88  func (f *ConsulFingerprint) initialize(req *FingerprintRequest) error {
    89  	// Only create the Consul client once to avoid creating many connections
    90  	if f.client == nil {
    91  		consulConfig, err := req.Config.ConsulConfig.ApiConfig()
    92  		if err != nil {
    93  			return fmt.Errorf("failed to initialize Consul client config: %v", err)
    94  		}
    95  
    96  		f.client, err = consulapi.NewClient(consulConfig)
    97  		if err != nil {
    98  			return fmt.Errorf("failed to initialize Consul client: %s", err)
    99  		}
   100  
   101  		f.extractors = map[string]consulExtractor{
   102  			"consul.server":        f.server,
   103  			"consul.version":       f.version,
   104  			"consul.sku":           f.sku,
   105  			"consul.revision":      f.revision,
   106  			"unique.consul.name":   f.name,
   107  			"consul.datacenter":    f.dc,
   108  			"consul.segment":       f.segment,
   109  			"consul.connect":       f.connect,
   110  			"consul.grpc":          f.grpc(consulConfig.Scheme),
   111  			"consul.ft.namespaces": f.namespaces,
   112  		}
   113  	}
   114  
   115  	return nil
   116  }
   117  
   118  func (f *ConsulFingerprint) query(resp *FingerprintResponse) agentconsul.Self {
   119  	// We'll try to detect consul by making a query to to the agent's self API.
   120  	// If we can't hit this URL consul is probably not running on this machine.
   121  	info, err := f.client.Agent().Self()
   122  	if err != nil {
   123  		// indicate consul no longer available
   124  		if f.lastState == consulAvailable {
   125  			f.logger.Info("consul agent is unavailable")
   126  		}
   127  		f.lastState = consulUnavailable
   128  		return nil
   129  	}
   130  	return info
   131  }
   132  
   133  func (f *ConsulFingerprint) link(resp *FingerprintResponse) {
   134  	if dc, ok := resp.Attributes["consul.datacenter"]; ok {
   135  		if name, ok2 := resp.Attributes["unique.consul.name"]; ok2 {
   136  			resp.AddLink("consul", fmt.Sprintf("%s.%s", dc, name))
   137  		}
   138  	} else {
   139  		f.logger.Warn("malformed Consul response prevented linking")
   140  	}
   141  }
   142  
   143  func (f *ConsulFingerprint) server(info agentconsul.Self) (string, bool) {
   144  	s, ok := info["Config"]["Server"].(bool)
   145  	return strconv.FormatBool(s), ok
   146  }
   147  
   148  func (f *ConsulFingerprint) version(info agentconsul.Self) (string, bool) {
   149  	v, ok := info["Config"]["Version"].(string)
   150  	return v, ok
   151  }
   152  
   153  func (f *ConsulFingerprint) sku(info agentconsul.Self) (string, bool) {
   154  	return agentconsul.SKU(info)
   155  }
   156  
   157  func (f *ConsulFingerprint) revision(info agentconsul.Self) (string, bool) {
   158  	r, ok := info["Config"]["Revision"].(string)
   159  	return r, ok
   160  }
   161  
   162  func (f *ConsulFingerprint) name(info agentconsul.Self) (string, bool) {
   163  	n, ok := info["Config"]["NodeName"].(string)
   164  	return n, ok
   165  }
   166  
   167  func (f *ConsulFingerprint) dc(info agentconsul.Self) (string, bool) {
   168  	d, ok := info["Config"]["Datacenter"].(string)
   169  	return d, ok
   170  }
   171  
   172  func (f *ConsulFingerprint) segment(info agentconsul.Self) (string, bool) {
   173  	tags, tagsOK := info["Member"]["Tags"].(map[string]interface{})
   174  	if !tagsOK {
   175  		return "", false
   176  	}
   177  	s, ok := tags["segment"].(string)
   178  	return s, ok
   179  }
   180  
   181  func (f *ConsulFingerprint) connect(info agentconsul.Self) (string, bool) {
   182  	c, ok := info["DebugConfig"]["ConnectEnabled"].(bool)
   183  	return strconv.FormatBool(c), ok
   184  }
   185  
   186  func (f *ConsulFingerprint) grpc(scheme string) func(info agentconsul.Self) (string, bool) {
   187  	return func(info agentconsul.Self) (string, bool) {
   188  
   189  		// The version is needed in order to understand which config object to
   190  		// query. This is because Consul 1.14.0 added a new gRPC port which
   191  		// broke the previous behaviour.
   192  		v, ok := info["Config"]["Version"].(string)
   193  		if !ok {
   194  			return "", false
   195  		}
   196  
   197  		consulVersion, err := version.NewVersion(v)
   198  		if err != nil {
   199  			return "", false
   200  		}
   201  
   202  		// If the Consul agent being fingerprinted is running a version less
   203  		// than 1.14.0 we use the original single gRPC port.
   204  		if consulVersion.Core().LessThan(consulGRPCPortChangeVersion.Core()) {
   205  			return f.grpcPort(info)
   206  		}
   207  
   208  		// Now that we know we are querying a Consul agent running v1.14.0 or
   209  		// greater, we need to select the correct port parameter from the
   210  		// config depending on whether we have been asked to speak TLS or not.
   211  		switch strings.ToLower(scheme) {
   212  		case "https":
   213  			return f.grpcTLSPort(info)
   214  		default:
   215  			return f.grpcPort(info)
   216  		}
   217  	}
   218  }
   219  
   220  func (f *ConsulFingerprint) grpcPort(info agentconsul.Self) (string, bool) {
   221  	p, ok := info["DebugConfig"]["GRPCPort"].(float64)
   222  	return fmt.Sprintf("%d", int(p)), ok
   223  }
   224  
   225  func (f *ConsulFingerprint) grpcTLSPort(info agentconsul.Self) (string, bool) {
   226  	p, ok := info["DebugConfig"]["GRPCTLSPort"].(float64)
   227  	return fmt.Sprintf("%d", int(p)), ok
   228  }
   229  
   230  func (f *ConsulFingerprint) namespaces(info agentconsul.Self) (string, bool) {
   231  	return strconv.FormatBool(agentconsul.Namespaces(info)), true
   232  }