From b5b97c22121e0afec52a424f34ad28481e27cbda Mon Sep 17 00:00:00 2001 From: Vladimir Dementyev Date: Wed, 1 Nov 2023 15:27:45 -0700 Subject: [PATCH] feat: smarter fly presets for clusters --- CHANGELOG.md | 4 + config/presets.go | 41 +++++++--- config/presets_test.go | 167 ++++++++++++++++++++++++++++++++--------- docs/configuration.md | 2 +- fly/fly.go | 71 ++++++++++++++++++ fly/fly_test.go | 30 ++++++++ go.mod | 9 ++- go.sum | 11 ++- mocks/dns.go | 52 +++++++++++++ 9 files changed, 339 insertions(+), 48 deletions(-) create mode 100644 fly/fly.go create mode 100644 fly/fly_test.go create mode 100644 mocks/dns.go diff --git a/CHANGELOG.md b/CHANGELOG.md index d4b5709a..85b4a6ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## master +- Infer default configuration for Fly apps based on the cluster formation. ([@palkan][]) + + Automatically enable broker (in-memory or embedded NATS-based) depending on the number of VMs in the cluster. + - Add NATS-based broker. ([@palkan][]) ## 1.4.6 (2023-10-25) diff --git a/config/presets.go b/config/presets.go index bf8afdb2..d8fa9f43 100644 --- a/config/presets.go +++ b/config/presets.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/anycable/anycable-go/fly" "github.com/apex/log" ) @@ -66,6 +67,18 @@ func (c *Config) loadFlyPreset(defaults *Config) error { return errors.New("FLY_APP_NAME env is missing") } + // Obtain cluster info + cluster, err := fly.Cluster(appName) + + if err != nil { + log.Debugf("Failed to retrieve Fly app info from DNS: %v", err) + } + + // Whether we can use embedded NATS broker with this setup + singleNode := cluster != nil && cluster.NumVMs() == 1 + multiNode := cluster != nil && cluster.NumVMs() > 1 + useNATSBroker := cluster != nil && cluster.NumRegions() == 1 && cluster.NumVMs() >= 3 + // Use the same port for HTTP broadcasts by default if c.HTTPBroadcast.Port == defaults.HTTPBroadcast.Port { c.HTTPBroadcast.Port = c.Port @@ -96,21 +109,31 @@ func (c *Config) loadFlyPreset(defaults *Config) error { if c.PubSubAdapter == defaults.PubSubAdapter { if c.Redis.URL != defaults.Redis.URL { c.PubSubAdapter = "redis" - } else { + } + } + + if multiNode { + if c.PubSubAdapter == defaults.PubSubAdapter { c.PubSubAdapter = "nats" + } + + // NATS hasn't been configured, so we can embed it + if !c.EmbedNats || c.NATS.Servers == defaults.NATS.Servers { + c.EmbedNats = true + c.NATS.Servers = c.EmbeddedNats.ServiceAddr + } - // NATS hasn't been configured, so we can embed it - if !c.EmbedNats || c.NATS.Servers == defaults.NATS.Servers { - c.EmbedNats = true - c.NATS.Servers = c.EmbeddedNats.ServiceAddr + if c.BrokerAdapter == defaults.BrokerAdapter { + if useNATSBroker { + log.WithField("context", "config").Infof("Discovered %d VMs in the same ragion -> enabling NATS broker", cluster.NumVMs()) + c.BrokerAdapter = "nats" } } } - if c.BrokerAdapter == defaults.BrokerAdapter { - if c.EmbedNats { - c.BrokerAdapter = "nats" - } + if singleNode { + log.WithField("context", "config").Infof("Discovered a single node cluster -> enabling in-memory broker") + c.BrokerAdapter = "memory" } if rpcName, ok := os.LookupEnv("ANYCABLE_FLY_RPC_APP_NAME"); ok { diff --git a/config/presets_test.go b/config/presets_test.go index d1a6edc7..0d46eeb3 100644 --- a/config/presets_test.go +++ b/config/presets_test.go @@ -4,6 +4,7 @@ import ( "os" "testing" + "github.com/anycable/anycable-go/mocks" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -18,7 +19,7 @@ func TestNoPresets(t *testing.T) { assert.Equal(t, "localhost", config.Host) } -func TestFlyPresets(t *testing.T) { +func TestFlyPresets_no_vms_discovered(t *testing.T) { cleanupEnv := setEnv( "FLY_APP_NAME", "any-test", "FLY_REGION", "mag", @@ -37,6 +38,104 @@ func TestFlyPresets(t *testing.T) { require.NoError(t, err) + assert.Equal(t, "0.0.0.0", config.Host) + assert.Equal(t, 8989, config.Port) + assert.Equal(t, 8989, config.HTTPBroadcast.Port) + // No cluster info, no broker — sorry + assert.Equal(t, "", config.BrokerAdapter) + assert.Equal(t, "nats://0.0.0.0:4222", config.EmbeddedNats.ServiceAddr) + assert.Equal(t, "nats://0.0.0.0:5222", config.EmbeddedNats.ClusterAddr) + assert.Equal(t, "any-test-mag-cluster", config.EmbeddedNats.ClusterName) + assert.Equal(t, []string{"nats://mag.any-test.internal:5222"}, config.EmbeddedNats.Routes) + assert.Equal(t, "dns:///mag.anycable-web.internal:50051", config.RPC.Host) +} + +func TestFlyPresets_when_single_vm_discovered(t *testing.T) { + teardownDNS := mocks.MockDNSServer("vms.any-test.internal.", []string{"1234 mag"}) + defer teardownDNS() + + cleanupEnv := setEnv( + "FLY_APP_NAME", "any-test", + "FLY_REGION", "mag", + "FLY_ALLOC_ID", "1234", + ) + defer cleanupEnv() + + config := NewConfig() + + config.Port = 8989 + + require.Equal(t, []string{"fly"}, config.Presets()) + + err := config.LoadPresets() + + require.NoError(t, err) + + assert.Equal(t, "0.0.0.0", config.Host) + assert.Equal(t, 8989, config.Port) + assert.Equal(t, 8989, config.HTTPBroadcast.Port) + // In-memory broker is good enough for single node, no pub/sub needed + assert.Equal(t, false, config.EmbedNats) + assert.Equal(t, "", config.PubSubAdapter) + assert.Equal(t, "memory", config.BrokerAdapter) + assert.Equal(t, "nats://0.0.0.0:4222", config.EmbeddedNats.ServiceAddr) +} + +func TestFlyPresets_when_two_vms_discovered(t *testing.T) { + teardownDNS := mocks.MockDNSServer("vms.any-test.internal.", []string{"1234 mag", "4567 mag"}) + defer teardownDNS() + + cleanupEnv := setEnv( + "FLY_APP_NAME", "any-test", + "FLY_REGION", "mag", + "FLY_ALLOC_ID", "1234", + ) + defer cleanupEnv() + + config := NewConfig() + + config.Port = 8989 + + require.Equal(t, []string{"fly"}, config.Presets()) + + err := config.LoadPresets() + + require.NoError(t, err) + + assert.Equal(t, "0.0.0.0", config.Host) + assert.Equal(t, 8989, config.Port) + assert.Equal(t, 8989, config.HTTPBroadcast.Port) + assert.Equal(t, true, config.EmbedNats) + assert.Equal(t, "nats", config.PubSubAdapter) + // We do not enable broker by default, since it requires at least 3 nodes or exactly 1 + assert.Equal(t, "", config.BrokerAdapter) + assert.Equal(t, "nats://0.0.0.0:4222", config.EmbeddedNats.ServiceAddr) + assert.Equal(t, "nats://0.0.0.0:5222", config.EmbeddedNats.ClusterAddr) + assert.Equal(t, "any-test-mag-cluster", config.EmbeddedNats.ClusterName) + assert.Equal(t, []string{"nats://mag.any-test.internal:5222"}, config.EmbeddedNats.Routes) +} + +func TestFlyPresets_when_three_vms_discovered(t *testing.T) { + teardownDNS := mocks.MockDNSServer("vms.any-test.internal.", []string{"1234 mag", "4567 mag", "8901 mag"}) + defer teardownDNS() + + cleanupEnv := setEnv( + "FLY_APP_NAME", "any-test", + "FLY_REGION", "mag", + "FLY_ALLOC_ID", "1234", + ) + defer cleanupEnv() + + config := NewConfig() + + config.Port = 8989 + + require.Equal(t, []string{"fly"}, config.Presets()) + + err := config.LoadPresets() + + require.NoError(t, err) + assert.Equal(t, "0.0.0.0", config.Host) assert.Equal(t, 8989, config.Port) assert.Equal(t, 8989, config.HTTPBroadcast.Port) @@ -47,7 +146,39 @@ func TestFlyPresets(t *testing.T) { assert.Equal(t, "nats://0.0.0.0:5222", config.EmbeddedNats.ClusterAddr) assert.Equal(t, "any-test-mag-cluster", config.EmbeddedNats.ClusterName) assert.Equal(t, []string{"nats://mag.any-test.internal:5222"}, config.EmbeddedNats.Routes) - assert.Equal(t, "dns:///mag.anycable-web.internal:50051", config.RPC.Host) +} + +func TestFlyPresets_when_three_vms_from_different_regions(t *testing.T) { + teardownDNS := mocks.MockDNSServer("vms.any-test.internal.", []string{"1234 mag", "4567 mag", "8901 sea"}) + defer teardownDNS() + + cleanupEnv := setEnv( + "FLY_APP_NAME", "any-test", + "FLY_REGION", "mag", + "FLY_ALLOC_ID", "1234", + ) + defer cleanupEnv() + + config := NewConfig() + + config.Port = 8989 + + require.Equal(t, []string{"fly"}, config.Presets()) + + err := config.LoadPresets() + + require.NoError(t, err) + + assert.Equal(t, "0.0.0.0", config.Host) + assert.Equal(t, 8989, config.Port) + assert.Equal(t, 8989, config.HTTPBroadcast.Port) + assert.Equal(t, true, config.EmbedNats) + assert.Equal(t, "nats", config.PubSubAdapter) + // Currently, we do not enable broker for multi-region setup; we need to figure this out later + assert.Equal(t, "", config.BrokerAdapter) + assert.Equal(t, "nats://0.0.0.0:4222", config.EmbeddedNats.ServiceAddr) + assert.Equal(t, "nats://0.0.0.0:5222", config.EmbeddedNats.ClusterAddr) + assert.Equal(t, "any-test-mag-cluster", config.EmbeddedNats.ClusterName) } func TestFlyPresets_When_RedisConfigured(t *testing.T) { @@ -147,38 +278,6 @@ func TestBrokerWhenENATSConfigured(t *testing.T) { assert.Equal(t, "nats", config.PubSubAdapter) } -func TestFlyWithBrokerPresets(t *testing.T) { - cleanupEnv := setEnv( - "FLY_APP_NAME", "any-test", - "FLY_REGION", "mag", - "FLY_ALLOC_ID", "1234", - ) - defer cleanupEnv() - - config := NewConfig() - config.UserPresets = []string{"fly", "broker"} - config.Port = 8989 - - require.Equal(t, []string{"fly", "broker"}, config.Presets()) - - err := config.LoadPresets() - - require.NoError(t, err) - - assert.Equal(t, "0.0.0.0", config.Host) - assert.Equal(t, 8989, config.Port) - assert.Equal(t, 8989, config.HTTPBroadcast.Port) - assert.Equal(t, true, config.EmbedNats) - assert.Equal(t, "nats", config.PubSubAdapter) - assert.Equal(t, "nats", config.BrokerAdapter) - assert.Equal(t, "http,nats", config.BroadcastAdapter) - assert.Equal(t, "nats://0.0.0.0:4222", config.EmbeddedNats.ServiceAddr) - assert.Equal(t, "nats://0.0.0.0:5222", config.EmbeddedNats.ClusterAddr) - assert.Equal(t, "any-test-mag-cluster", config.EmbeddedNats.ClusterName) - assert.Equal(t, []string{"nats://mag.any-test.internal:5222"}, config.EmbeddedNats.Routes) - assert.Equal(t, "localhost:50051", config.RPC.Host) -} - func TestOverrideSomePresetSettings(t *testing.T) { cleanupEnv := setEnv( "FLY_APP_NAME", "any-test", diff --git a/docs/configuration.md b/docs/configuration.md index 1c8f4c06..88781d2e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -137,7 +137,7 @@ The preset provide the following defaults: - `enats_cluster_routes`: "nats://\.\.internal:5222" - `enats_gateway_advertise`: "\.\.internal:7222" (**NOTE:** You must set `ANYCABLE_ENATS_GATEWAY` to `nats://0.0.0.0:7222` and configure at least one gateway address manually to enable gateways). -Also, [embedded NATS](./embedded_nats.md) is enabled automatically if no other pub/sub adapter neither Redis is configured. Similarly, pub/sub, broker and broadcast adapters using embedded NATS are configured automatically, too. Thus, by default, AnyCable-Go setups a NATS cluster automatically (within a single region), no configuration is required. +Also, [embedded NATS](./embedded_nats.md) is enabled automatically if no other pub/sub adapter neither Redis is configured. If the number of VMs is at least 3 and all within the same region, the NATS-based broker is enabled, so you can have [reliable streams](./reliable_streams.md) automatically. If the `ANYCABLE_FLY_RPC_APP_NAME` env variable is provided, the following defaults are configured as well: diff --git a/fly/fly.go b/fly/fly.go new file mode 100644 index 00000000..94464c5f --- /dev/null +++ b/fly/fly.go @@ -0,0 +1,71 @@ +// Utilities to interact with Fly.io platform +package fly + +import ( + "context" + "fmt" + "net" + "time" +) + +type VMInfo struct { + ID string + Region string +} + +// ClusterInfo contains information about the Fly.io cluster +// obtained from the DNS records, such as the number of machines and regions +type ClusterInfo struct { + regions []string + vms []*VMInfo +} + +func (c *ClusterInfo) NumRegions() int { + return len(c.regions) +} + +func (c *ClusterInfo) Regions() []string { + return c.regions +} + +func (c *ClusterInfo) VMs() []string { + ids := make([]string, len(c.vms)) + for i, vm := range c.vms { + ids[i] = vm.ID + } + return ids +} + +func (c *ClusterInfo) NumVMs() int { + return len(c.vms) +} + +func Cluster(appName string) (*ClusterInfo, error) { + addr := fmt.Sprintf("vms.%s.internal.", appName) + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + textRecords, err := net.DefaultResolver.LookupTXT(ctx, addr) + + if err != nil { + return nil, err + } + + vms := make([]*VMInfo, len(textRecords)) + regionsMap := make(map[string]struct{}) + regions := make([]string, 0) + + for i, txt := range textRecords { + vm := &VMInfo{} + fmt.Sscanf(txt, "%s %s", &vm.ID, &vm.Region) + vms[i] = vm + + if _, ok := regionsMap[vm.Region]; !ok { + regionsMap[vm.Region] = struct{}{} + regions = append(regions, vm.Region) + } + } + + return &ClusterInfo{regions: regions, vms: vms}, nil +} diff --git a/fly/fly_test.go b/fly/fly_test.go new file mode 100644 index 00000000..5a7ff43e --- /dev/null +++ b/fly/fly_test.go @@ -0,0 +1,30 @@ +package fly + +import ( + "testing" + + "github.com/anycable/anycable-go/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCluster_multiple_nodes_and_regions(t *testing.T) { + teardownDNS := mocks.MockDNSServer("vms.my-fly-app.internal.", []string{"xyz ewr", "abc sea", "def ewr"}) + defer teardownDNS() + + cluster, err := Cluster("my-fly-app") + + require.NoError(t, err) + + assert.Equal(t, 2, cluster.NumRegions()) + assert.Equal(t, 3, cluster.NumVMs()) + assert.EqualValues(t, []string{"ewr", "sea"}, cluster.Regions()) + assert.EqualValues(t, []string{"xyz", "abc", "def"}, cluster.VMs()) +} + +func TestCluster_when_dns_error(t *testing.T) { + cluster, err := Cluster("my-fly-app") + + require.Error(t, err) + assert.Nil(t, cluster) +} diff --git a/go.mod b/go.mod index 23b091c9..abd20e90 100644 --- a/go.mod +++ b/go.mod @@ -29,18 +29,23 @@ require ( github.com/urfave/cli/v2 v2.11.1 go.uber.org/automaxprocs v1.5.3 golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df - golang.org/x/net v0.12.0 + golang.org/x/net v0.15.0 google.golang.org/grpc v1.53.0 ) -require github.com/sony/gobreaker v0.5.0 +require ( + github.com/miekg/dns v1.1.56 + github.com/sony/gobreaker v0.5.0 +) require ( github.com/klauspost/compress v1.17.2 // indirect github.com/minio/highwayhash v1.0.2 // indirect github.com/nats-io/jwt/v2 v2.5.2 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect + golang.org/x/mod v0.12.0 // indirect golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.13.0 // indirect ) require ( diff --git a/go.sum b/go.sum index cadd7deb..d084a160 100644 --- a/go.sum +++ b/go.sum @@ -217,6 +217,8 @@ github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lL github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= +github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -363,6 +365,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -383,8 +387,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -397,6 +401,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -444,6 +449,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/mocks/dns.go b/mocks/dns.go new file mode 100644 index 00000000..76467dd0 --- /dev/null +++ b/mocks/dns.go @@ -0,0 +1,52 @@ +package mocks + +import ( + context "context" + "net" + "time" + + "github.com/miekg/dns" +) + +func MockDNSServer(zone string, txt []string) func() { + dns.HandleFunc(zone, func(w dns.ResponseWriter, r *dns.Msg) { + m := new(dns.Msg) + m.SetReply(r) + for _, txt := range txt { + m.Answer = append(m.Answer, &dns.TXT{ + Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 0}, + Txt: []string{txt}, + }) + } + w.WriteMsg(m) // nolint:errcheck + }) + + wait := make(chan struct{}) + + server := &dns.Server{Addr: ":0", Net: "udp"} + server.NotifyStartedFunc = func() { + close(wait) + } + + go server.ListenAndServe() // nolint:errcheck + + <-wait + + addr := server.PacketConn.LocalAddr().String() + + net.DefaultResolver = &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{ + Timeout: time.Millisecond * 500, + } + return d.DialContext(ctx, "udp", addr) + }, + } + + return func() { + net.DefaultResolver.PreferGo = false + net.DefaultResolver.Dial = nil + server.Shutdown() // nolint:errcheck + } +}