vitess.io/vitess@v0.16.2/go/vt/vtadmin/cluster/discovery/discovery_consul.go (about) 1 /* 2 Copyright 2020 The Vitess Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package discovery 18 19 import ( 20 "context" 21 "fmt" 22 "math/rand" 23 "strings" 24 "text/template" 25 "time" 26 27 consul "github.com/hashicorp/consul/api" 28 "github.com/spf13/pflag" 29 30 "vitess.io/vitess/go/textutil" 31 "vitess.io/vitess/go/trace" 32 33 vtadminpb "vitess.io/vitess/go/vt/proto/vtadmin" 34 ) 35 36 // ConsulDiscovery implements the Discovery interface for consul. 37 type ConsulDiscovery struct { 38 cluster *vtadminpb.Cluster 39 client ConsulClient 40 queryOptions *consul.QueryOptions 41 42 /* misc options */ 43 passingOnly bool 44 45 /* vtgate options */ 46 vtgateDatacenter string 47 vtgateService string 48 vtgatePoolTag string 49 vtgateCellTag string 50 vtgateKeyspacesToWatchTag string 51 vtgateAddrTmpl *template.Template 52 vtgateFQDNTmpl *template.Template 53 54 /* vtctld options */ 55 vtctldDatacenter string 56 vtctldService string 57 vtctldAddrTmpl *template.Template 58 vtctldFQDNTmpl *template.Template 59 } 60 61 // NewConsul returns a ConsulDiscovery for the given cluster. Args are a slice 62 // of command-line flags (e.g. "-key=value") that are parsed by a consul- 63 // specific flag set. 64 func NewConsul(cluster *vtadminpb.Cluster, flags *pflag.FlagSet, args []string) (Discovery, error) { // nolint:funlen 65 c, err := consul.NewClient(consul.DefaultConfig()) 66 if err != nil { 67 return nil, err 68 } 69 70 qopts := &consul.QueryOptions{ 71 AllowStale: false, 72 RequireConsistent: true, 73 WaitIndex: uint64(0), 74 UseCache: true, 75 } 76 77 disco := &ConsulDiscovery{ 78 cluster: cluster, 79 client: &consulClient{c}, 80 queryOptions: qopts, 81 } 82 83 flags.DurationVar(&disco.queryOptions.MaxAge, "max-age", time.Second*30, 84 "how old a cached value can be before consul queries stop using it") 85 flags.StringVar(&disco.queryOptions.Token, "token", "", "consul ACL token to use for requests") 86 flags.BoolVar(&disco.passingOnly, "passing-only", true, "whether to include only nodes passing healthchecks") 87 88 /* vtgate discovery config options */ 89 flags.StringVar(&disco.vtgateService, "vtgate-service-name", "vtgate", "consul service name vtgates register as") 90 flags.StringVar(&disco.vtgatePoolTag, "vtgate-pool-tag", "pool", "consul service tag to group vtgates by pool") 91 flags.StringVar(&disco.vtgateCellTag, "vtgate-cell-tag", "cell", "consul service tag to group vtgates by cell") 92 flags.StringVar(&disco.vtgateKeyspacesToWatchTag, "vtgate-keyspaces-to-watch-tag", "keyspaces", 93 "consul service tag identifying -keyspaces_to_watch for vtgates") 94 95 vtgateAddrTmplStr := flags.String("vtgate-addr-tmpl", "{{ .Hostname }}", 96 "Go template string to produce a dialable address from a *vtadminpb.VTGate "+ 97 "NOTE: the .FQDN field will never be set in the addr template context.") 98 vtgateDatacenterTmplStr := flags.String("vtgate-datacenter-tmpl", "", 99 "Go template string to generate the datacenter for vtgate consul queries. "+ 100 "The meta information about the cluster is provided to the template via {{ .Cluster }}. "+ 101 "Used once during initialization.") 102 vtgateFQDNTmplStr := flags.String("vtgate-fqdn-tmpl", "", 103 "Optional Go template string to produce an FQDN to access the vtgate from a browser. "+ 104 "E.g. \"{{ .Hostname }}.example.com\".") 105 106 /* vtctld discovery config options */ 107 flags.StringVar(&disco.vtctldService, "vtctld-service-name", "vtctld", "consul service name vtctlds register as") 108 109 vtctldAddrTmplStr := flags.String("vtctld-addr-tmpl", "{{ .Hostname }}", 110 "Go template string to produce a dialable address from a *vtadminpb.Vtctld "+ 111 "NOTE: the .FQDN field will never be set in the addr template context.") 112 vtctldDatacenterTmplStr := flags.String("vtctld-datacenter-tmpl", "", 113 "Go template string to generate the datacenter for vtgate consul queries. "+ 114 "The cluster name is provided to the template via {{ .Cluster }}. "+ 115 "Used once during initialization.") 116 vtctldFQDNTmplStr := flags.String("vtctld-fqdn-tmpl", "", 117 "Optional Go template string to produce an FQDN to access the vtctld from a browser. "+ 118 "E.g. \"{{ .Hostname }}.example.com\".") 119 120 if err := flags.Parse(args); err != nil { 121 return nil, err 122 } 123 124 /* gates options */ 125 if *vtgateDatacenterTmplStr != "" { 126 disco.vtgateDatacenter, err = generateConsulDatacenter("vtgate", cluster, *vtgateDatacenterTmplStr) 127 if err != nil { 128 return nil, fmt.Errorf("failed to generate vtgate consul datacenter from template: %w", err) 129 } 130 } 131 132 if *vtgateFQDNTmplStr != "" { 133 disco.vtgateFQDNTmpl, err = template.New("consul-vtgate-fqdn-template-" + cluster.Id).Parse(*vtgateFQDNTmplStr) 134 if err != nil { 135 return nil, fmt.Errorf("failed to parse vtgate FQDN template %s: %w", *vtgateFQDNTmplStr, err) 136 } 137 } 138 139 disco.vtgateAddrTmpl, err = template.New("consul-vtgate-address-template-" + cluster.Id).Parse(*vtgateAddrTmplStr) 140 if err != nil { 141 return nil, fmt.Errorf("failed to parse vtgate host address template %s: %w", *vtgateAddrTmplStr, err) 142 } 143 144 /* vtctld options */ 145 if *vtctldDatacenterTmplStr != "" { 146 disco.vtctldDatacenter, err = generateConsulDatacenter("vtctld", cluster, *vtctldDatacenterTmplStr) 147 if err != nil { 148 return nil, fmt.Errorf("failed to generate vtctld consul datacenter from template: %w", err) 149 } 150 } 151 152 if *vtctldFQDNTmplStr != "" { 153 disco.vtctldFQDNTmpl, err = template.New("consul-vtctld-fqdn-template-" + cluster.Id).Parse(*vtctldFQDNTmplStr) 154 if err != nil { 155 return nil, fmt.Errorf("failed to parse vtctld FQDN template %s: %w", *vtctldFQDNTmplStr, err) 156 } 157 } 158 159 disco.vtctldAddrTmpl, err = template.New("consul-vtctld-address-template-" + cluster.Id).Parse(*vtctldAddrTmplStr) 160 if err != nil { 161 return nil, fmt.Errorf("failed to parse vtctld host address template %s: %w", *vtctldAddrTmplStr, err) 162 } 163 164 return disco, nil 165 } 166 167 func generateConsulDatacenter(component string, cluster *vtadminpb.Cluster, tmplStr string) (string, error) { 168 tmpl, err := template.New("consul-" + component + "-datacenter-" + cluster.Id).Parse(tmplStr) 169 if err != nil { 170 return "", fmt.Errorf("error parsing template %s: %w", tmplStr, err) 171 } 172 173 dc, err := textutil.ExecuteTemplate(tmpl, &struct { 174 Cluster *vtadminpb.Cluster 175 }{ 176 Cluster: cluster, 177 }) 178 179 if err != nil { 180 return "", fmt.Errorf("failed to execute template: %w", err) 181 } 182 183 return dc, nil 184 } 185 186 // DiscoverVTGate is part of the Discovery interface. 187 func (c *ConsulDiscovery) DiscoverVTGate(ctx context.Context, tags []string) (*vtadminpb.VTGate, error) { 188 span, ctx := trace.NewSpan(ctx, "ConsulDiscovery.DiscoverVTGate") 189 defer span.Finish() 190 191 executeFQDNTemplate := true 192 193 return c.discoverVTGate(ctx, tags, executeFQDNTemplate) 194 } 195 196 // discoverVTGate calls discoverVTGates and then returns a random VTGate from 197 // the result. see discoverVTGates for further documentation. 198 func (c *ConsulDiscovery) discoverVTGate(ctx context.Context, tags []string, executeFQDNTemplate bool) (*vtadminpb.VTGate, error) { 199 vtgates, err := c.discoverVTGates(ctx, tags, executeFQDNTemplate) 200 if err != nil { 201 return nil, err 202 } 203 204 if len(vtgates) == 0 { 205 return nil, ErrNoVTGates 206 } 207 208 return vtgates[rand.Intn(len(vtgates))], nil 209 } 210 211 // DiscoverVTGateAddr is part of the Discovery interface. 212 func (c *ConsulDiscovery) DiscoverVTGateAddr(ctx context.Context, tags []string) (string, error) { 213 span, ctx := trace.NewSpan(ctx, "ConsulDiscovery.DiscoverVTGateAddr") 214 defer span.Finish() 215 216 executeFQDNTemplate := false 217 218 vtgate, err := c.discoverVTGate(ctx, tags, executeFQDNTemplate) 219 if err != nil { 220 return "", err 221 } 222 223 addr, err := textutil.ExecuteTemplate(c.vtgateAddrTmpl, vtgate) 224 if err != nil { 225 return "", fmt.Errorf("failed to execute vtgate address template for %v: %w", vtgate, err) 226 } 227 228 return addr, nil 229 } 230 231 // DiscoverVTGateAddrs is part of the Discovery interface. 232 func (c *ConsulDiscovery) DiscoverVTGateAddrs(ctx context.Context, tags []string) ([]string, error) { 233 span, ctx := trace.NewSpan(ctx, "ConsulDiscovery.DiscoverVTGateAddrs") 234 defer span.Finish() 235 236 executeFQDNTemplate := false 237 238 vtgates, err := c.discoverVTGates(ctx, tags, executeFQDNTemplate) 239 if err != nil { 240 return nil, err 241 } 242 243 addrs := make([]string, len(vtgates)) 244 for i, vtgate := range vtgates { 245 addr, err := textutil.ExecuteTemplate(c.vtgateAddrTmpl, vtgate) 246 if err != nil { 247 return nil, fmt.Errorf("failed to execute vtgate address template for %v: %w", vtgate, err) 248 } 249 250 addrs[i] = addr 251 } 252 253 return addrs, nil 254 } 255 256 // DiscoverVTGates is part of the Discovery interface. 257 func (c *ConsulDiscovery) DiscoverVTGates(ctx context.Context, tags []string) ([]*vtadminpb.VTGate, error) { 258 span, ctx := trace.NewSpan(ctx, "ConsulDiscovery.DiscoverVTGates") 259 defer span.Finish() 260 261 executeFQDNTemplate := true 262 263 return c.discoverVTGates(ctx, tags, executeFQDNTemplate) 264 } 265 266 // discoverVTGates does the actual work of discovering VTGate hosts from a 267 // consul datacenter. executeFQDNTemplate is boolean to allow an optimization 268 // for DiscoverVTGateAddr (the only function that sets the boolean to false). 269 func (c *ConsulDiscovery) discoverVTGates(_ context.Context, tags []string, executeFQDNTemplate bool) ([]*vtadminpb.VTGate, error) { 270 opts := c.getQueryOptions() 271 opts.Datacenter = c.vtgateDatacenter 272 273 entries, _, err := c.client.Health().ServiceMultipleTags(c.vtgateService, tags, c.passingOnly, &opts) 274 if err != nil { 275 return nil, err 276 } 277 278 vtgates := make([]*vtadminpb.VTGate, len(entries)) 279 280 for i, entry := range entries { 281 vtgate := &vtadminpb.VTGate{ 282 Hostname: entry.Node.Node, 283 Cluster: &vtadminpb.Cluster{ 284 Id: c.cluster.Id, 285 Name: c.cluster.Name, 286 }, 287 } 288 289 var cell, pool string 290 for _, tag := range entry.Service.Tags { 291 if pool != "" && cell != "" { 292 break 293 } 294 295 parts := strings.Split(tag, ":") 296 if len(parts) != 2 { 297 continue 298 } 299 300 name, value := parts[0], parts[1] 301 switch name { 302 case c.vtgateCellTag: 303 cell = value 304 case c.vtgatePoolTag: 305 pool = value 306 } 307 } 308 309 vtgate.Cell = cell 310 vtgate.Pool = pool 311 312 if keyspaces, ok := entry.Service.Meta[c.vtgateKeyspacesToWatchTag]; ok { 313 vtgate.Keyspaces = strings.Split(keyspaces, ",") 314 } 315 316 if executeFQDNTemplate { 317 if c.vtgateFQDNTmpl != nil { 318 vtgate.FQDN, err = textutil.ExecuteTemplate(c.vtgateFQDNTmpl, vtgate) 319 if err != nil { 320 return nil, fmt.Errorf("failed to execute vtgate fqdn template for %v: %w", vtgate, err) 321 } 322 } 323 } 324 325 vtgates[i] = vtgate 326 } 327 328 return vtgates, nil 329 } 330 331 // DiscoverVtctld is part of the Discovery interface. 332 func (c *ConsulDiscovery) DiscoverVtctld(ctx context.Context, tags []string) (*vtadminpb.Vtctld, error) { 333 span, ctx := trace.NewSpan(ctx, "ConsulDiscovery.DiscoverVtctld") 334 defer span.Finish() 335 336 executeFQDNTemplate := true 337 338 return c.discoverVtctld(ctx, tags, executeFQDNTemplate) 339 } 340 341 // discoverVtctld calls discoverVtctlds and then returns a random vtctld from 342 // the result. see discoverVtctlds for further documentation. 343 func (c *ConsulDiscovery) discoverVtctld(ctx context.Context, tags []string, executeFQDNTemplate bool) (*vtadminpb.Vtctld, error) { 344 vtctlds, err := c.discoverVtctlds(ctx, tags, executeFQDNTemplate) 345 if err != nil { 346 return nil, err 347 } 348 349 if len(vtctlds) == 0 { 350 return nil, ErrNoVtctlds 351 } 352 353 return vtctlds[rand.Intn(len(vtctlds))], nil 354 } 355 356 // DiscoverVtctldAddr is part of the Discovery interface. 357 func (c *ConsulDiscovery) DiscoverVtctldAddr(ctx context.Context, tags []string) (string, error) { 358 span, ctx := trace.NewSpan(ctx, "ConsulDiscovery.DiscoverVtctldAddr") 359 defer span.Finish() 360 361 executeFQDNTemplate := false 362 363 vtctld, err := c.discoverVtctld(ctx, tags, executeFQDNTemplate) 364 if err != nil { 365 return "", err 366 } 367 368 addr, err := textutil.ExecuteTemplate(c.vtctldAddrTmpl, vtctld) 369 if err != nil { 370 return "", fmt.Errorf("failed to execute vtctld address template for %v: %w", vtctld, err) 371 } 372 373 return addr, nil 374 } 375 376 // DiscoverVtctldAddrs is part of the Discovery interface. 377 func (c *ConsulDiscovery) DiscoverVtctldAddrs(ctx context.Context, tags []string) ([]string, error) { 378 span, ctx := trace.NewSpan(ctx, "ConsulDiscovery.DiscoverVtctldAddrs") 379 defer span.Finish() 380 381 executeFQDNTemplate := false 382 383 vtctlds, err := c.discoverVtctlds(ctx, tags, executeFQDNTemplate) 384 if err != nil { 385 return nil, err 386 } 387 388 addrs := make([]string, len(vtctlds)) 389 for i, vtctld := range vtctlds { 390 addr, err := textutil.ExecuteTemplate(c.vtctldAddrTmpl, vtctld) 391 if err != nil { 392 return nil, fmt.Errorf("failed to execute vtctld address template for %v: %w", vtctld, err) 393 } 394 395 addrs[i] = addr 396 } 397 398 return addrs, nil 399 } 400 401 // DiscoverVtctlds is part of the Discovery interface. 402 func (c *ConsulDiscovery) DiscoverVtctlds(ctx context.Context, tags []string) ([]*vtadminpb.Vtctld, error) { 403 span, ctx := trace.NewSpan(ctx, "ConsulDiscovery.DiscoverVtctlds") 404 defer span.Finish() 405 406 executeFQDNTemplate := true 407 408 return c.discoverVtctlds(ctx, tags, executeFQDNTemplate) 409 } 410 411 // discoverVtctlds does the actual work of discovering Vtctld hosts from a 412 // consul datacenter. executeFQDNTemplate is boolean to allow an optimization 413 // for DiscoverVtctldAddr (the only function that sets the boolean to false). 414 func (c *ConsulDiscovery) discoverVtctlds(_ context.Context, tags []string, executeFQDNTemplate bool) ([]*vtadminpb.Vtctld, error) { 415 opts := c.getQueryOptions() 416 opts.Datacenter = c.vtctldDatacenter 417 418 entries, _, err := c.client.Health().ServiceMultipleTags(c.vtctldService, tags, c.passingOnly, &opts) 419 if err != nil { 420 return nil, err 421 } 422 423 vtctlds := make([]*vtadminpb.Vtctld, len(entries)) 424 425 for i, entry := range entries { 426 vtctld := &vtadminpb.Vtctld{ 427 Cluster: &vtadminpb.Cluster{ 428 Id: c.cluster.Id, 429 Name: c.cluster.Name, 430 }, 431 Hostname: entry.Node.Node, 432 } 433 434 if executeFQDNTemplate { 435 if c.vtctldFQDNTmpl != nil { 436 vtctld.FQDN, err = textutil.ExecuteTemplate(c.vtctldFQDNTmpl, vtctld) 437 if err != nil { 438 return nil, fmt.Errorf("failed to execute vtctld fqdn template for %v: %w", vtctld, err) 439 } 440 } 441 } 442 443 vtctlds[i] = vtctld 444 } 445 446 return vtctlds, nil 447 } 448 449 // getQueryOptions returns a shallow copy so we can swap in the vtgateDatacenter. 450 // If we were to set it directly, we'd need a mutex to guard against concurrent 451 // vtgate and (soon) vtctld queries. 452 func (c *ConsulDiscovery) getQueryOptions() consul.QueryOptions { 453 if c.queryOptions == nil { 454 return consul.QueryOptions{} 455 } 456 457 opts := *c.queryOptions 458 459 return opts 460 } 461 462 // ConsulClient defines an interface for the subset of the consul API used by 463 // discovery, so we can swap in an implementation for testing. 464 type ConsulClient interface { 465 Health() ConsulHealth 466 } 467 468 // ConsulHealth defines an interface for the subset of the (*consul.Health) struct 469 // used by discovery, so we can swap in an implementation for testing. 470 type ConsulHealth interface { 471 ServiceMultipleTags(service string, tags []string, passingOnly bool, q *consul.QueryOptions) ([]*consul.ServiceEntry, *consul.QueryMeta, error) // nolint:lll 472 } 473 474 // consulClient is our shim wrapper around the upstream consul client. 475 type consulClient struct { 476 *consul.Client 477 } 478 479 func (c *consulClient) Health() ConsulHealth { 480 return c.Client.Health() 481 }