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 }