vitess.io/vitess@v0.16.2/go/vt/vtadmin/cluster/discovery/discovery_consul_test.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  	"sort"
    22  	"testing"
    23  	"text/template"
    24  
    25  	consul "github.com/hashicorp/consul/api"
    26  	"github.com/stretchr/testify/assert"
    27  
    28  	vtadminpb "vitess.io/vitess/go/vt/proto/vtadmin"
    29  )
    30  
    31  type fakeConsulClient struct {
    32  	health *fakeConsulHealth
    33  }
    34  
    35  func (c *fakeConsulClient) Health() ConsulHealth { return c.health }
    36  
    37  type fakeConsulHealth struct {
    38  	entries map[string][]*consul.ServiceEntry
    39  }
    40  
    41  func (health *fakeConsulHealth) ServiceMultipleTags(service string, tags []string, passingOnly bool, q *consul.QueryOptions) ([]*consul.ServiceEntry, *consul.QueryMeta, error) { // nolint:lll
    42  	if health.entries == nil {
    43  		return nil, nil, assert.AnError
    44  	}
    45  
    46  	sort.Strings(tags)
    47  
    48  	serviceEntries, ok := health.entries[service]
    49  	if !ok {
    50  		return []*consul.ServiceEntry{}, nil, nil
    51  	}
    52  
    53  	filterByTags := func(etags []string) bool {
    54  		sort.Strings(etags)
    55  
    56  		for _, tag := range tags {
    57  			i := sort.SearchStrings(etags, tag)
    58  			if i >= len(etags) || etags[i] != tag {
    59  				return false
    60  			}
    61  		}
    62  
    63  		return true
    64  	}
    65  
    66  	filteredEntries := make([]*consul.ServiceEntry, 0, len(serviceEntries))
    67  
    68  	for _, entry := range serviceEntries {
    69  		if filterByTags(append([]string{}, entry.Service.Tags...)) { // we take a copy here to not mutate the original slice
    70  			filteredEntries = append(filteredEntries, entry)
    71  		}
    72  	}
    73  
    74  	return filteredEntries, nil, nil
    75  }
    76  
    77  func consulServiceEntry(name string, tags []string, meta map[string]string) *consul.ServiceEntry {
    78  	return &consul.ServiceEntry{
    79  		Node: &consul.Node{
    80  			Node: name,
    81  		},
    82  		Service: &consul.AgentService{
    83  			Meta: meta,
    84  			Tags: tags,
    85  		},
    86  	}
    87  }
    88  
    89  func TestConsulDiscoverVTGates(t *testing.T) {
    90  	t.Parallel()
    91  
    92  	tests := []struct {
    93  		name      string
    94  		disco     *ConsulDiscovery
    95  		tags      []string
    96  		entries   map[string][]*consul.ServiceEntry
    97  		expected  []*vtadminpb.VTGate
    98  		shouldErr bool
    99  	}{
   100  		{
   101  			name: "all gates",
   102  			disco: &ConsulDiscovery{
   103  				cluster: &vtadminpb.Cluster{
   104  					Id:   "cid",
   105  					Name: "cluster",
   106  				},
   107  				vtgateService: "vtgate",
   108  				vtgateCellTag: "cell",
   109  				vtgatePoolTag: "pool",
   110  			},
   111  			tags: []string{},
   112  			entries: map[string][]*consul.ServiceEntry{
   113  				"vtgate": {
   114  					consulServiceEntry("vtgate1", []string{"pool:pool1", "cell:zone1", "extra:tag"}, nil),
   115  					consulServiceEntry("vtgate2", []string{"pool:pool1", "cell:zone2"}, nil),
   116  					consulServiceEntry("vtgate3", []string{"pool:pool1", "cell:zone3"}, nil),
   117  				},
   118  			},
   119  			expected: []*vtadminpb.VTGate{
   120  				{
   121  					Cluster: &vtadminpb.Cluster{
   122  						Id:   "cid",
   123  						Name: "cluster",
   124  					},
   125  					Hostname: "vtgate1",
   126  					Cell:     "zone1",
   127  					Pool:     "pool1",
   128  				},
   129  				{
   130  					Cluster: &vtadminpb.Cluster{
   131  						Id:   "cid",
   132  						Name: "cluster",
   133  					},
   134  					Hostname: "vtgate2",
   135  					Cell:     "zone2",
   136  					Pool:     "pool1",
   137  				},
   138  				{
   139  					Cluster: &vtadminpb.Cluster{
   140  						Id:   "cid",
   141  						Name: "cluster",
   142  					},
   143  					Hostname: "vtgate3",
   144  					Cell:     "zone3",
   145  					Pool:     "pool1",
   146  				},
   147  			},
   148  			shouldErr: false,
   149  		},
   150  		{
   151  			name: "one cell",
   152  			disco: &ConsulDiscovery{
   153  				cluster: &vtadminpb.Cluster{
   154  					Id:   "cid",
   155  					Name: "cluster",
   156  				},
   157  				vtgateService: "vtgate",
   158  				vtgateCellTag: "cell",
   159  				vtgatePoolTag: "pool",
   160  			},
   161  			tags: []string{"cell:zone1"},
   162  			entries: map[string][]*consul.ServiceEntry{
   163  				"vtgate": {
   164  					consulServiceEntry("vtgate1", []string{"pool:pool1", "cell:zone1", "extra:tag"}, nil),
   165  					consulServiceEntry("vtgate2", []string{"pool:pool1", "cell:zone2"}, nil),
   166  					consulServiceEntry("vtgate3", []string{"pool:pool1", "cell:zone3"}, nil),
   167  				},
   168  			},
   169  			expected: []*vtadminpb.VTGate{
   170  				{
   171  					Cluster: &vtadminpb.Cluster{
   172  						Id:   "cid",
   173  						Name: "cluster",
   174  					},
   175  					Hostname: "vtgate1",
   176  					Cell:     "zone1",
   177  					Pool:     "pool1",
   178  				},
   179  			},
   180  			shouldErr: false,
   181  		},
   182  		{
   183  			name: "keyspaces to watch",
   184  			disco: &ConsulDiscovery{
   185  				cluster: &vtadminpb.Cluster{
   186  					Id:   "cid",
   187  					Name: "cluster",
   188  				},
   189  				vtgateService:             "vtgate",
   190  				vtgateCellTag:             "cell",
   191  				vtgatePoolTag:             "pool",
   192  				vtgateKeyspacesToWatchTag: "keyspaces",
   193  			},
   194  			tags: []string{},
   195  			entries: map[string][]*consul.ServiceEntry{
   196  				"vtgate": {
   197  					consulServiceEntry("vtgate1", []string{"pool:pool1", "cell:zone1"}, map[string]string{"keyspaces": "ks1,ks2"}),
   198  				},
   199  			},
   200  			expected: []*vtadminpb.VTGate{
   201  				{
   202  					Cluster: &vtadminpb.Cluster{
   203  						Id:   "cid",
   204  						Name: "cluster",
   205  					},
   206  					Hostname:  "vtgate1",
   207  					Cell:      "zone1",
   208  					Pool:      "pool1",
   209  					Keyspaces: []string{"ks1", "ks2"},
   210  				},
   211  			},
   212  			shouldErr: false,
   213  		},
   214  		{
   215  			name: "error",
   216  			disco: &ConsulDiscovery{
   217  				cluster: &vtadminpb.Cluster{
   218  					Id:   "cid",
   219  					Name: "cluster",
   220  				},
   221  				vtgateService:             "vtgate",
   222  				vtgateCellTag:             "cell",
   223  				vtgatePoolTag:             "pool",
   224  				vtgateKeyspacesToWatchTag: "keyspaces",
   225  			},
   226  			tags:      []string{},
   227  			entries:   nil,
   228  			expected:  []*vtadminpb.VTGate{},
   229  			shouldErr: true,
   230  		},
   231  	}
   232  
   233  	ctx := context.Background()
   234  
   235  	for _, tt := range tests {
   236  		tt := tt
   237  
   238  		t.Run(tt.name, func(t *testing.T) {
   239  			t.Parallel()
   240  
   241  			tt.disco.client = &fakeConsulClient{
   242  				health: &fakeConsulHealth{
   243  					entries: tt.entries,
   244  				},
   245  			}
   246  
   247  			gates, err := tt.disco.DiscoverVTGates(ctx, tt.tags)
   248  			if tt.shouldErr {
   249  				assert.Error(t, err, assert.AnError)
   250  				return
   251  			}
   252  
   253  			assert.NoError(t, err)
   254  			assert.Equal(t, tt.expected, gates)
   255  		})
   256  	}
   257  }
   258  
   259  func TestConsulDiscoverVTGate(t *testing.T) {
   260  	t.Parallel()
   261  
   262  	tests := []struct {
   263  		name      string
   264  		disco     *ConsulDiscovery
   265  		tags      []string
   266  		entries   map[string][]*consul.ServiceEntry
   267  		expected  *vtadminpb.VTGate
   268  		shouldErr bool
   269  	}{
   270  		{
   271  			name: "success",
   272  			disco: &ConsulDiscovery{
   273  				cluster: &vtadminpb.Cluster{
   274  					Id:   "cid",
   275  					Name: "cluster",
   276  				},
   277  				vtgateService: "vtgate",
   278  				vtgateCellTag: "cell",
   279  				vtgatePoolTag: "pool",
   280  			},
   281  			tags: []string{"cell:zone1"},
   282  			entries: map[string][]*consul.ServiceEntry{
   283  				"vtgate": {
   284  					consulServiceEntry("vtgate1", []string{"pool:pool1", "cell:zone1"}, nil),
   285  					consulServiceEntry("vtgate2", []string{"pool:pool1", "cell:zone2"}, nil),
   286  					consulServiceEntry("vtgate3", []string{"pool:pool1", "cell:zone3"}, nil),
   287  				},
   288  			},
   289  			expected: &vtadminpb.VTGate{
   290  				Cluster: &vtadminpb.Cluster{
   291  					Id:   "cid",
   292  					Name: "cluster",
   293  				},
   294  				Hostname: "vtgate1",
   295  				Cell:     "zone1",
   296  				Pool:     "pool1",
   297  			},
   298  			shouldErr: false,
   299  		},
   300  		{
   301  			name: "no gates",
   302  			disco: &ConsulDiscovery{
   303  				cluster: &vtadminpb.Cluster{
   304  					Id:   "cid",
   305  					Name: "cluster",
   306  				},
   307  				vtgateService: "vtgate",
   308  				vtgateCellTag: "cell",
   309  				vtgatePoolTag: "pool",
   310  			},
   311  			tags: []string{"cell:zone1"},
   312  			entries: map[string][]*consul.ServiceEntry{
   313  				"vtgate": {},
   314  			},
   315  			expected: &vtadminpb.VTGate{
   316  				Cluster: &vtadminpb.Cluster{
   317  					Id:   "cid",
   318  					Name: "cluster",
   319  				},
   320  				Hostname: "vtgate1",
   321  				Cell:     "zone1",
   322  				Pool:     "pool1",
   323  			},
   324  			shouldErr: true,
   325  		},
   326  		{
   327  			name: "error",
   328  			disco: &ConsulDiscovery{
   329  				cluster: &vtadminpb.Cluster{
   330  					Id:   "cid",
   331  					Name: "cluster",
   332  				},
   333  				vtgateService: "vtgate",
   334  				vtgateCellTag: "cell",
   335  				vtgatePoolTag: "pool",
   336  			},
   337  			tags:      []string{"cell:zone1"},
   338  			entries:   nil,
   339  			expected:  nil,
   340  			shouldErr: true,
   341  		},
   342  	}
   343  
   344  	ctx := context.Background()
   345  
   346  	for _, tt := range tests {
   347  		tt := tt
   348  
   349  		t.Run(tt.name, func(t *testing.T) {
   350  			t.Parallel()
   351  
   352  			tt.disco.client = &fakeConsulClient{
   353  				health: &fakeConsulHealth{
   354  					entries: tt.entries,
   355  				},
   356  			}
   357  
   358  			gate, err := tt.disco.DiscoverVTGate(ctx, tt.tags)
   359  			if tt.shouldErr {
   360  				assert.Error(t, err, assert.AnError)
   361  				return
   362  			}
   363  
   364  			assert.NoError(t, err)
   365  			assert.Equal(t, tt.expected, gate)
   366  		})
   367  	}
   368  }
   369  
   370  func TestConsulDiscoverVTGateAddr(t *testing.T) {
   371  	t.Parallel()
   372  
   373  	tests := []struct {
   374  		name      string
   375  		disco     *ConsulDiscovery
   376  		tags      []string
   377  		entries   map[string][]*consul.ServiceEntry
   378  		expected  string
   379  		shouldErr bool
   380  	}{
   381  		{
   382  			name: "default template",
   383  			disco: &ConsulDiscovery{
   384  				cluster: &vtadminpb.Cluster{
   385  					Id:   "cid",
   386  					Name: "cluster",
   387  				},
   388  				vtgateService:  "vtgate",
   389  				vtgateCellTag:  "cell",
   390  				vtgatePoolTag:  "pool",
   391  				vtgateAddrTmpl: template.Must(template.New("").Parse("{{ .Hostname }}")),
   392  			},
   393  			tags: []string{},
   394  			entries: map[string][]*consul.ServiceEntry{
   395  				"vtgate": {
   396  					consulServiceEntry("vtgate1", []string{"pool:pool1", "cell:zone1"}, nil),
   397  				},
   398  			},
   399  			expected:  "vtgate1",
   400  			shouldErr: false,
   401  		},
   402  		{
   403  			name: "custom template",
   404  			disco: &ConsulDiscovery{
   405  				cluster: &vtadminpb.Cluster{
   406  					Id:   "cid",
   407  					Name: "cluster",
   408  				},
   409  				vtgateService:  "vtgate",
   410  				vtgateCellTag:  "cell",
   411  				vtgatePoolTag:  "pool",
   412  				vtgateAddrTmpl: template.Must(template.New("").Parse("{{ .Cluster.Name }}-{{ .Pool }}-{{ .Cell }}-{{ .Hostname }}.example.com:15000")), // nolint:lll
   413  			},
   414  			tags: []string{},
   415  			entries: map[string][]*consul.ServiceEntry{
   416  				"vtgate": {
   417  					consulServiceEntry("vtgate1", []string{"pool:pool1", "cell:zone1"}, nil),
   418  				},
   419  			},
   420  			expected:  "cluster-pool1-zone1-vtgate1.example.com:15000",
   421  			shouldErr: false,
   422  		},
   423  		{
   424  			name: "error",
   425  			disco: &ConsulDiscovery{
   426  				cluster: &vtadminpb.Cluster{
   427  					Id:   "cid",
   428  					Name: "cluster",
   429  				},
   430  				vtgateService:  "vtgate",
   431  				vtgateCellTag:  "cell",
   432  				vtgatePoolTag:  "pool",
   433  				vtgateAddrTmpl: template.Must(template.New("").Parse("{{ .Hostname }}")),
   434  			},
   435  			tags:      []string{},
   436  			entries:   nil,
   437  			expected:  "",
   438  			shouldErr: true,
   439  		},
   440  	}
   441  
   442  	ctx := context.Background()
   443  
   444  	for _, tt := range tests {
   445  		tt := tt
   446  
   447  		t.Run(tt.name, func(t *testing.T) {
   448  			t.Parallel()
   449  
   450  			tt.disco.client = &fakeConsulClient{
   451  				health: &fakeConsulHealth{
   452  					entries: tt.entries,
   453  				},
   454  			}
   455  
   456  			addr, err := tt.disco.DiscoverVTGateAddr(ctx, tt.tags)
   457  			if tt.shouldErr {
   458  				assert.Error(t, err, assert.AnError)
   459  				return
   460  			}
   461  
   462  			assert.NoError(t, err)
   463  			assert.Equal(t, tt.expected, addr)
   464  		})
   465  	}
   466  }