From 3bfb75b270905dbe5dc696ae3bf29077f33f4ed3 Mon Sep 17 00:00:00 2001 From: Sara Bee <855595+doeg@users.noreply.github.com> Date: Mon, 28 Dec 2020 12:10:57 -0500 Subject: [PATCH 1/4] Add static file service discovery implementation Signed-off-by: Sara Bee <855595+doeg@users.noreply.github.com> --- docker/local/run.sh | 2 +- go/vt/vtadmin/cluster/discovery/discovery.go | 1 + .../discovery/discovery_static_file.go | 161 ++++++++++++ .../discovery/discovery_static_file_test.go | 231 ++++++++++++++++++ 4 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 go/vt/vtadmin/cluster/discovery/discovery_static_file.go create mode 100644 go/vt/vtadmin/cluster/discovery/discovery_static_file_test.go diff --git a/docker/local/run.sh b/docker/local/run.sh index 1774d091d05..217052d4304 100755 --- a/docker/local/run.sh +++ b/docker/local/run.sh @@ -1,3 +1,3 @@ #!/bin/bash -docker run -p 15000:15000 -p 15001:15001 --rm -it vitess/local +docker run -p 15000:15000 -p 15001:15001 -p 15991:15991 --rm -it vitess/local diff --git a/go/vt/vtadmin/cluster/discovery/discovery.go b/go/vt/vtadmin/cluster/discovery/discovery.go index 25cb4fac8fe..3dbcb41f996 100644 --- a/go/vt/vtadmin/cluster/discovery/discovery.go +++ b/go/vt/vtadmin/cluster/discovery/discovery.go @@ -91,4 +91,5 @@ func New(impl string, cluster *vtadminpb.Cluster, args []string) (Discovery, err func init() { // nolint:gochecknoinits Register("consul", NewConsul) + Register("staticFile", NewStaticFile) } diff --git a/go/vt/vtadmin/cluster/discovery/discovery_static_file.go b/go/vt/vtadmin/cluster/discovery/discovery_static_file.go new file mode 100644 index 00000000000..1f4b634460d --- /dev/null +++ b/go/vt/vtadmin/cluster/discovery/discovery_static_file.go @@ -0,0 +1,161 @@ +/* +Copyright 2020 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package discovery + +import ( + "context" + "encoding/json" + "errors" + "io/ioutil" + "math/rand" + + "github.com/spf13/pflag" + vtadminpb "vitess.io/vitess/go/vt/proto/vtadmin" +) + +// StaticFileDiscovery implements the Discovery interface for "discovering" +// Vitess components hardcoded in a static .json file. +type StaticFileDiscovery struct { + cluster *vtadminpb.Cluster + config *StaticFileClusterConfig + gates struct { + byName map[string]*vtadminpb.VTGate + byTag map[string][]*vtadminpb.VTGate + } +} + +// StaticFileClusterConfig configures Vitess components for a single cluster. +type StaticFileClusterConfig struct { + VTGates []*StaticFileVTGateConfig `json:"vtgates,omitempty"` +} + +// StaticFileVTGateConfig contains host and tag information for a single VTGate in a cluster. +type StaticFileVTGateConfig struct { + Host *vtadminpb.VTGate `json:"host"` + Tags []string `json:"tags"` +} + +// NewStaticFile returns a StaticFileDiscovery for the given cluster. +func NewStaticFile(cluster *vtadminpb.Cluster, flags *pflag.FlagSet, args []string) (Discovery, error) { + disco := &StaticFileDiscovery{ + cluster: cluster, + } + + filePath := flags.String("path", "", "path to the service discovery JSON config file") + if err := flags.Parse(args); err != nil { + return nil, err + } + + if filePath == nil || *filePath == "" { + return nil, errors.New("must specify path to the service discovery JSON config file") + } + + b, err := ioutil.ReadFile(*filePath) + if err != nil { + return nil, err + } + + if err := disco.parseConfig(b); err != nil { + return nil, err + } + + return disco, nil +} + +func (d *StaticFileDiscovery) parseConfig(bytes []byte) error { + if err := json.Unmarshal(bytes, &d.config); err != nil { + return err + } + + d.gates.byName = make(map[string]*vtadminpb.VTGate, len(d.config.VTGates)) + d.gates.byTag = make(map[string][]*vtadminpb.VTGate) + + // Index the gates by name and by tag for easier lookups + for _, gate := range d.config.VTGates { + d.gates.byName[gate.Host.Hostname] = gate.Host + + for _, tag := range gate.Tags { + d.gates.byTag[tag] = append(d.gates.byTag[tag], gate.Host) + } + } + return nil +} + +// DiscoverVTGate is part of the Discovery interface. +func (d *StaticFileDiscovery) DiscoverVTGate(ctx context.Context, tags []string) (*vtadminpb.VTGate, error) { + gates, err := d.DiscoverVTGates(ctx, tags) + if err != nil { + return nil, err + } + + count := len(gates) + if count == 0 { + return nil, ErrNoVTGates + } + + gate := gates[rand.Intn(len(gates))] + return gate, nil +} + +// DiscoverVTGateAddr is part of the Discovery interface. +func (d *StaticFileDiscovery) DiscoverVTGateAddr(ctx context.Context, tags []string) (string, error) { + gate, err := d.DiscoverVTGate(ctx, tags) + if err != nil { + return "", err + } + + return gate.Hostname, nil +} + +// DiscoverVTGates is part of the Discovery interface. +func (d *StaticFileDiscovery) DiscoverVTGates(ctx context.Context, tags []string) ([]*vtadminpb.VTGate, error) { + if len(tags) == 0 { + results := []*vtadminpb.VTGate{} + for _, g := range d.gates.byName { + results = append(results, g) + } + + return results, nil + } + + set := d.gates.byName + + for _, tag := range tags { + intermediate := map[string]*vtadminpb.VTGate{} + + gates, ok := d.gates.byTag[tag] + if !ok { + return []*vtadminpb.VTGate{}, nil + } + + for _, g := range gates { + if _, ok := set[g.Hostname]; ok { + intermediate[g.Hostname] = g + } + } + + set = intermediate + } + + results := make([]*vtadminpb.VTGate, 0, len(set)) + + for _, gate := range set { + results = append(results, gate) + } + + return results, nil +} diff --git a/go/vt/vtadmin/cluster/discovery/discovery_static_file_test.go b/go/vt/vtadmin/cluster/discovery/discovery_static_file_test.go new file mode 100644 index 00000000000..54b9b7c5b7c --- /dev/null +++ b/go/vt/vtadmin/cluster/discovery/discovery_static_file_test.go @@ -0,0 +1,231 @@ +/* +Copyright 2020 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package discovery + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "vitess.io/vitess/go/vt/proto/vtadmin" + vtadminpb "vitess.io/vitess/go/vt/proto/vtadmin" +) + +func TestDiscoverVTGate(t *testing.T) { + tests := []struct { + name string + contents []byte + expected *vtadminpb.VTGate + tags []string + shouldErr bool + }{ + { + name: "empty config", + contents: []byte(`{}`), + expected: nil, + shouldErr: true, + }, + { + name: "one gate", + contents: []byte(` + { + "vtgates": [{ + "host": { + "hostname": "127.0.0.1:12345" + } + }] + } + `), + expected: &vtadmin.VTGate{ + Hostname: "127.0.0.1:12345", + }, + }, + { + name: "filtered by tags (one match)", + contents: []byte(` + { + "vtgates": [ + { + "host": { + "hostname": "127.0.0.1:11111" + }, + "tags": ["cell:cellA"] + }, + { + "host": { + "hostname": "127.0.0.1:22222" + }, + "tags": ["cell:cellB"] + }, + { + "host": { + "hostname": "127.0.0.1:33333" + }, + "tags": ["cell:cellA"] + } + ] + } + `), + expected: &vtadminpb.VTGate{ + Hostname: "127.0.0.1:22222", + }, + tags: []string{"cell:cellB"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + disco := &StaticFileDiscovery{} + err := disco.parseConfig(tt.contents) + require.NoError(t, err) + + gate, err := disco.DiscoverVTGate(context.Background(), tt.tags) + if tt.shouldErr { + assert.Error(t, err, assert.AnError) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.expected, gate) + }) + } +} + +func TestDiscoverVTGates(t *testing.T) { + tests := []struct { + name string + contents []byte + tags []string + expected []*vtadminpb.VTGate + shouldErr bool + }{ + { + name: "empty config", + contents: []byte(`{}`), + expected: []*vtadminpb.VTGate{}, + shouldErr: false, + }, + { + name: "no tags", + contents: []byte(` + { + "vtgates": [ + { + "host": { + "hostname": "127.0.0.1:12345" + } + }, + { + "host": { + "hostname": "127.0.0.1:67890" + } + } + ] + } + `), + expected: []*vtadminpb.VTGate{ + {Hostname: "127.0.0.1:12345"}, + {Hostname: "127.0.0.1:67890"}, + }, + shouldErr: false, + }, + { + name: "filtered by tags", + contents: []byte(` + { + "vtgates": [ + { + "host": { + "hostname": "127.0.0.1:11111" + }, + "tags": ["cell:cellA"] + }, + { + "host": { + "hostname": "127.0.0.1:22222" + }, + "tags": ["cell:cellB"] + }, + { + "host": { + "hostname": "127.0.0.1:33333" + }, + "tags": ["cell:cellA"] + } + ] + } + `), + tags: []string{"cell:cellA"}, + expected: []*vtadminpb.VTGate{ + {Hostname: "127.0.0.1:11111"}, + {Hostname: "127.0.0.1:33333"}, + }, + shouldErr: false, + }, + { + name: "filtered by multiple tags", + contents: []byte(` + { + "vtgates": [ + { + "host": { + "hostname": "127.0.0.1:11111" + }, + "tags": ["cell:cellA"] + }, + { + "host": { + "hostname": "127.0.0.1:22222" + }, + "tags": ["cell:cellA", "pool:poolZ"] + }, + { + "host": { + "hostname": "127.0.0.1:33333" + }, + "tags": ["pool:poolZ"] + } + ] + } + `), + tags: []string{"cell:cellA", "pool:poolZ"}, + expected: []*vtadminpb.VTGate{ + {Hostname: "127.0.0.1:22222"}, + }, + shouldErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + disco := &StaticFileDiscovery{} + + err := disco.parseConfig(tt.contents) + require.NoError(t, err) + + gates, err := disco.DiscoverVTGates(context.Background(), tt.tags) + if tt.shouldErr { + assert.Error(t, err, assert.AnError) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.expected, gates) + }) + } +} From 0a7df91ab8ef8767994b5186871aeae9daee70d0 Mon Sep 17 00:00:00 2001 From: Sara Bee <855595+doeg@users.noreply.github.com> Date: Mon, 28 Dec 2020 12:54:10 -0500 Subject: [PATCH 2/4] Assert ElementsMatch instead of Equal for slices returned by DiscoverVTGates in discovery_static_file_test.go Signed-off-by: Sara Bee <855595+doeg@users.noreply.github.com> --- go/vt/vtadmin/cluster/discovery/discovery_static_file_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/vt/vtadmin/cluster/discovery/discovery_static_file_test.go b/go/vt/vtadmin/cluster/discovery/discovery_static_file_test.go index 54b9b7c5b7c..addab84b578 100644 --- a/go/vt/vtadmin/cluster/discovery/discovery_static_file_test.go +++ b/go/vt/vtadmin/cluster/discovery/discovery_static_file_test.go @@ -225,7 +225,7 @@ func TestDiscoverVTGates(t *testing.T) { } assert.NoError(t, err) - assert.Equal(t, tt.expected, gates) + assert.ElementsMatch(t, tt.expected, gates) }) } } From 75cb52bab6a037d507cd5819898c0fe406db68c5 Mon Sep 17 00:00:00 2001 From: Sara Bee <855595+doeg@users.noreply.github.com> Date: Mon, 28 Dec 2020 13:38:55 -0500 Subject: [PATCH 3/4] Add a case with malformed json to TestDiscoverVTGates Signed-off-by: Sara Bee <855595+doeg@users.noreply.github.com> --- .../discovery/discovery_static_file_test.go | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/go/vt/vtadmin/cluster/discovery/discovery_static_file_test.go b/go/vt/vtadmin/cluster/discovery/discovery_static_file_test.go index addab84b578..efd14e2b3de 100644 --- a/go/vt/vtadmin/cluster/discovery/discovery_static_file_test.go +++ b/go/vt/vtadmin/cluster/discovery/discovery_static_file_test.go @@ -108,11 +108,14 @@ func TestDiscoverVTGate(t *testing.T) { func TestDiscoverVTGates(t *testing.T) { tests := []struct { - name string - contents []byte - tags []string - expected []*vtadminpb.VTGate + name string + contents []byte + tags []string + expected []*vtadminpb.VTGate + // True if the test should produce an error on the DiscoverVTGates call shouldErr bool + // True if the test should produce an error on the disco.parseConfig step + shouldErrConfig bool }{ { name: "empty config", @@ -154,7 +157,7 @@ func TestDiscoverVTGates(t *testing.T) { "hostname": "127.0.0.1:11111" }, "tags": ["cell:cellA"] - }, + }, { "host": { "hostname": "127.0.0.1:22222" @@ -187,7 +190,7 @@ func TestDiscoverVTGates(t *testing.T) { "hostname": "127.0.0.1:11111" }, "tags": ["cell:cellA"] - }, + }, { "host": { "hostname": "127.0.0.1:22222" @@ -209,6 +212,17 @@ func TestDiscoverVTGates(t *testing.T) { }, shouldErr: false, }, + { + name: "invalid json", + contents: []byte(` + { + "vtgates": "malformed" + } + `), + tags: []string{}, + shouldErr: false, + shouldErrConfig: true, + }, } for _, tt := range tests { @@ -216,7 +230,11 @@ func TestDiscoverVTGates(t *testing.T) { disco := &StaticFileDiscovery{} err := disco.parseConfig(tt.contents) - require.NoError(t, err) + if tt.shouldErrConfig { + assert.Error(t, err, assert.AnError) + } else { + require.NoError(t, err) + } gates, err := disco.DiscoverVTGates(context.Background(), tt.tags) if tt.shouldErr { From e2d6b2ac7363b7525c215807542a813ab1fd314d Mon Sep 17 00:00:00 2001 From: Sara Bee <855595+doeg@users.noreply.github.com> Date: Mon, 28 Dec 2020 13:47:07 -0500 Subject: [PATCH 4/4] Add example JSON to discovery_static_file.go Signed-off-by: Sara Bee <855595+doeg@users.noreply.github.com> --- .../cluster/discovery/discovery_static_file.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/go/vt/vtadmin/cluster/discovery/discovery_static_file.go b/go/vt/vtadmin/cluster/discovery/discovery_static_file.go index 1f4b634460d..7b32fc74931 100644 --- a/go/vt/vtadmin/cluster/discovery/discovery_static_file.go +++ b/go/vt/vtadmin/cluster/discovery/discovery_static_file.go @@ -28,7 +28,22 @@ import ( ) // StaticFileDiscovery implements the Discovery interface for "discovering" -// Vitess components hardcoded in a static .json file. +// Vitess components hardcoded in a static JSON file. +// +// As an example, here's a minimal JSON file for a single Vitess cluster running locally +// (such as the one described in https://vitess.io/docs/get-started/local-docker): +// +// { +// "vtgates": [ +// { +// "host": { +// "hostname": "127.0.0.1:15991" +// } +// } +// ] +// } +// +// For more examples of various static file configurations, see the unit tests. type StaticFileDiscovery struct { cluster *vtadminpb.Cluster config *StaticFileClusterConfig