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  }