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 }