diff --git a/cmd/config.go b/cmd/config.go
index 54c7cddbd22..de37f6a44bf 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -35,6 +35,7 @@ import (
"github.com/loadimpact/k6/lib"
"github.com/loadimpact/k6/lib/executor"
+ "github.com/loadimpact/k6/lib/types"
"github.com/loadimpact/k6/stats"
"github.com/loadimpact/k6/stats/cloud"
"github.com/loadimpact/k6/stats/csv"
@@ -259,6 +260,17 @@ func applyDefault(conf Config) Config {
if conf.Options.SummaryTrendStats == nil {
conf.Options.SummaryTrendStats = lib.DefaultSummaryTrendStats
}
+ defDNS := types.DefaultDNSConfig()
+ if !conf.DNS.TTL.Valid {
+ conf.DNS.TTL = defDNS.TTL
+ }
+ if !conf.DNS.Select.Valid {
+ conf.DNS.Select = defDNS.Select
+ }
+ if !conf.DNS.Policy.Valid {
+ conf.DNS.Policy = defDNS.Policy
+ }
+
return conf
}
diff --git a/cmd/config_consolidation_test.go b/cmd/config_consolidation_test.go
index b11763c7197..40e01e88ff0 100644
--- a/cmd/config_consolidation_test.go
+++ b/cmd/config_consolidation_test.go
@@ -139,7 +139,7 @@ type file struct {
func getFS(files []file) afero.Fs {
fs := afero.NewMemMapFs()
for _, f := range files {
- must(afero.WriteFile(fs, f.filepath, []byte(f.contents), 0644)) // modes don't matter in the afero.MemMapFs
+ must(afero.WriteFile(fs, f.filepath, []byte(f.contents), 0o644)) // modes don't matter in the afero.MemMapFs
}
return fs
}
@@ -214,11 +214,13 @@ func getConfigConsolidationTestCases() []configConsolidationTestCase {
{opts{cli: []string{"-u", "3", "-d", "30s"}}, exp{}, verifyConstLoopingVUs(I(3), 30*time.Second)},
{opts{cli: []string{"-u", "4", "--duration", "60s"}}, exp{}, verifyConstLoopingVUs(I(4), 1*time.Minute)},
{
- opts{cli: []string{"--stage", "20s:10", "-s", "3m:5"}}, exp{},
+ opts{cli: []string{"--stage", "20s:10", "-s", "3m:5"}},
+ exp{},
verifyRampingVUs(null.NewInt(1, false), buildStages(20, 10, 180, 5)),
},
{
- opts{cli: []string{"-s", "1m6s:5", "--vus", "10"}}, exp{},
+ opts{cli: []string{"-s", "1m6s:5", "--vus", "10"}},
+ exp{},
verifyRampingVUs(null.NewInt(10, true), buildStages(66, 5)),
},
{opts{cli: []string{"-u", "1", "-i", "6", "-d", "10s"}}, exp{}, func(t *testing.T, c Config) {
@@ -248,11 +250,13 @@ func getConfigConsolidationTestCases() []configConsolidationTestCase {
{opts{env: []string{"K6_VUS=5", "K6_ITERATIONS=15"}}, exp{}, verifySharedIters(I(5), I(15))},
{opts{env: []string{"K6_VUS=10", "K6_DURATION=20s"}}, exp{}, verifyConstLoopingVUs(I(10), 20*time.Second)},
{
- opts{env: []string{"K6_STAGES=2m30s:11,1h1m:100"}}, exp{},
+ opts{env: []string{"K6_STAGES=2m30s:11,1h1m:100"}},
+ exp{},
verifyRampingVUs(null.NewInt(1, false), buildStages(150, 11, 3660, 100)),
},
{
- opts{env: []string{"K6_STAGES=100s:100,0m30s:0", "K6_VUS=0"}}, exp{},
+ opts{env: []string{"K6_STAGES=100s:100,0m30s:0", "K6_VUS=0"}},
+ exp{},
verifyRampingVUs(null.NewInt(0, true), buildStages(100, 100, 30, 0)),
},
// Test if JSON configs work as expected
@@ -275,14 +279,16 @@ func getConfigConsolidationTestCases() []configConsolidationTestCase {
env: []string{"K6_DURATION=15s"},
cli: []string{"--stage", ""},
},
- exp{logWarning: true}, verifyOneIterPerOneVU,
+ exp{logWarning: true},
+ verifyOneIterPerOneVU,
},
{
opts{
runner: &lib.Options{VUs: null.IntFrom(5), Duration: types.NullDurationFrom(50 * time.Second)},
cli: []string{"--stage", "5s:5"},
},
- exp{}, verifyRampingVUs(I(5), buildStages(5, 5)),
+ exp{},
+ verifyRampingVUs(I(5), buildStages(5, 5)),
},
{
opts{
@@ -323,7 +329,8 @@ func getConfigConsolidationTestCases() []configConsolidationTestCase {
env: []string{"K6_ITERATIONS=25"},
cli: []string{"--vus", "12"},
},
- exp{}, verifySharedIters(I(12), I(25)),
+ exp{},
+ verifySharedIters(I(12), I(25)),
},
// TODO: test the externally controlled executor
@@ -375,6 +382,86 @@ func getConfigConsolidationTestCases() []configConsolidationTestCase {
assert.Equal(t, []string{"avg", "p(90)", "count"}, c.Options.SummaryTrendStats)
},
},
+ {opts{cli: []string{}}, exp{}, func(t *testing.T, c Config) {
+ assert.Equal(t, types.DNSConfig{
+ TTL: null.NewString("5m", false),
+ Select: types.NullDNSSelect{DNSSelect: types.DNSrandom, Valid: false},
+ Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false},
+ }, c.Options.DNS)
+ }},
+ {opts{env: []string{"K6_DNS=ttl=5,select=roundRobin"}}, exp{}, func(t *testing.T, c Config) {
+ assert.Equal(t, types.DNSConfig{
+ TTL: null.StringFrom("5"),
+ Select: types.NullDNSSelect{DNSSelect: types.DNSroundRobin, Valid: true},
+ Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false},
+ }, c.Options.DNS)
+ }},
+ {opts{env: []string{"K6_DNS=ttl=inf,select=random,policy=preferIPv6"}}, exp{}, func(t *testing.T, c Config) {
+ assert.Equal(t, types.DNSConfig{
+ TTL: null.StringFrom("inf"),
+ Select: types.NullDNSSelect{DNSSelect: types.DNSrandom, Valid: true},
+ Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv6, Valid: true},
+ }, c.Options.DNS)
+ }},
+ // This is functionally invalid, but will error out in validation done in js.parseTTL().
+ {opts{cli: []string{"--dns", "ttl=-1"}}, exp{}, func(t *testing.T, c Config) {
+ assert.Equal(t, types.DNSConfig{
+ TTL: null.StringFrom("-1"),
+ Select: types.NullDNSSelect{DNSSelect: types.DNSrandom, Valid: false},
+ Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false},
+ }, c.Options.DNS)
+ }},
+ {opts{cli: []string{"--dns", "ttl=0,blah=nope"}}, exp{cliReadError: true}, nil},
+ {opts{cli: []string{"--dns", "ttl=0"}}, exp{}, func(t *testing.T, c Config) {
+ assert.Equal(t, types.DNSConfig{
+ TTL: null.StringFrom("0"),
+ Select: types.NullDNSSelect{DNSSelect: types.DNSrandom, Valid: false},
+ Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false},
+ }, c.Options.DNS)
+ }},
+ {opts{cli: []string{"--dns", "ttl=5s,select="}}, exp{cliReadError: true}, nil},
+ {
+ opts{fs: defaultConfig(`{"dns": {"ttl": "0", "select": "roundRobin", "policy": "onlyIPv4"}}`)},
+ exp{},
+ func(t *testing.T, c Config) {
+ assert.Equal(t, types.DNSConfig{
+ TTL: null.StringFrom("0"),
+ Select: types.NullDNSSelect{DNSSelect: types.DNSroundRobin, Valid: true},
+ Policy: types.NullDNSPolicy{DNSPolicy: types.DNSonlyIPv4, Valid: true},
+ }, c.Options.DNS)
+ },
+ },
+ {
+ opts{
+ fs: defaultConfig(`{"dns": {"ttl": "0"}}`),
+ env: []string{"K6_DNS=ttl=30,policy=any"},
+ },
+ exp{},
+ func(t *testing.T, c Config) {
+ assert.Equal(t, types.DNSConfig{
+ TTL: null.StringFrom("30"),
+ Select: types.NullDNSSelect{DNSSelect: types.DNSrandom, Valid: false},
+ Policy: types.NullDNSPolicy{DNSPolicy: types.DNSany, Valid: true},
+ }, c.Options.DNS)
+ },
+ },
+ {
+ // CLI overrides all, falling back to env
+ opts{
+ fs: defaultConfig(`{"dns": {"ttl": "60", "select": "first"}}`),
+ env: []string{"K6_DNS=ttl=30,select=random,policy=any"},
+ cli: []string{"--dns", "ttl=5"},
+ },
+ exp{},
+ func(t *testing.T, c Config) {
+ assert.Equal(t, types.DNSConfig{
+ TTL: null.StringFrom("5"),
+ Select: types.NullDNSSelect{DNSSelect: types.DNSrandom, Valid: true},
+ Policy: types.NullDNSPolicy{DNSPolicy: types.DNSany, Valid: true},
+ }, c.Options.DNS)
+ },
+ },
+
// TODO: test for differences between flagsets
// TODO: more tests in general, especially ones not related to execution parameters...
}
diff --git a/cmd/options.go b/cmd/options.go
index 17bf1b477ca..0276b4daaab 100644
--- a/cmd/options.go
+++ b/cmd/options.go
@@ -93,6 +93,11 @@ func optionFlagSet() *pflag.FlagSet {
flags.StringSlice("tag", nil, "add a `tag` to be applied to all samples, as `[name]=[value]`")
flags.String("console-output", "", "redirects the console logging to the provided output file")
flags.Bool("discard-response-bodies", false, "Read but don't process or save HTTP response bodies")
+ flags.String("dns", types.DefaultDNSConfig().String(), "DNS resolver configuration. Possible ttl values are: 'inf' "+
+ "for a persistent cache, '0' to disable the cache,\nor a positive duration, e.g. '1s', '1m', etc. "+
+ "Milliseconds are assumed if no unit is provided.\n"+
+ "Possible select values to return a single IP are: 'first', 'random' or 'roundRobin'.\n"+
+ "Possible policy values are: 'preferIPv4', 'preferIPv6', 'onlyIPv4', 'onlyIPv6' or 'any'.\n")
return flags
}
@@ -248,6 +253,14 @@ func getOptions(flags *pflag.FlagSet) (lib.Options, error) {
opts.ConsoleOutput = null.StringFrom(redirectConFile)
}
+ if dns, err := flags.GetString("dns"); err != nil {
+ return opts, err
+ } else if dns != "" {
+ if err := opts.DNS.UnmarshalText([]byte(dns)); err != nil {
+ return opts, err
+ }
+ }
+
return opts, nil
}
diff --git a/core/local/local_test.go b/core/local/local_test.go
index c7ace16d503..7b958df9028 100644
--- a/core/local/local_test.go
+++ b/core/local/local_test.go
@@ -24,6 +24,7 @@ import (
"context"
"errors"
"fmt"
+ "io/ioutil"
"net"
"net/url"
"reflect"
@@ -47,6 +48,7 @@ import (
"github.com/loadimpact/k6/lib/testutils"
"github.com/loadimpact/k6/lib/testutils/httpmultibin"
"github.com/loadimpact/k6/lib/testutils/minirunner"
+ "github.com/loadimpact/k6/lib/testutils/mockresolver"
"github.com/loadimpact/k6/lib/types"
"github.com/loadimpact/k6/loader"
"github.com/loadimpact/k6/stats"
@@ -974,6 +976,106 @@ func TestExecutionSchedulerIsRunning(t *testing.T) {
assert.NoError(t, <-err)
}
+// TestDNSResolver checks the DNS resolution behavior at the ExecutionScheduler level.
+func TestDNSResolver(t *testing.T) {
+ tb := httpmultibin.NewHTTPMultiBin(t)
+ defer tb.Cleanup()
+ sr := tb.Replacer.Replace
+ script := sr(`
+ import http from "k6/http";
+ import { sleep } from "k6";
+
+ export let options = {
+ vus: 1,
+ iterations: 8,
+ noConnectionReuse: true,
+ }
+
+ export default function () {
+ const res = http.get("http://myhost:HTTPBIN_PORT/", { timeout: 50 });
+ sleep(0.7); // somewhat uneven multiple of 0.5 to minimize races with asserts
+ }`)
+
+ t.Run("cache", func(t *testing.T) {
+ testCases := map[string]struct {
+ opts lib.Options
+ expLogEntries int
+ }{
+ "default": { // IPs are cached for 5m
+ lib.Options{DNS: types.DefaultDNSConfig()}, 0,
+ },
+ "0": { // cache is disabled, every request does a DNS lookup
+ lib.Options{DNS: types.DNSConfig{
+ TTL: null.StringFrom("0"),
+ Select: types.NullDNSSelect{DNSSelect: types.DNSfirst, Valid: true},
+ Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false},
+ }}, 5,
+ },
+ "1000": { // cache IPs for 1s, check that unitless values are interpreted as ms
+ lib.Options{DNS: types.DNSConfig{
+ TTL: null.StringFrom("1000"),
+ Select: types.NullDNSSelect{DNSSelect: types.DNSfirst, Valid: true},
+ Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false},
+ }}, 4,
+ },
+ "3s": {
+ lib.Options{DNS: types.DNSConfig{
+ TTL: null.StringFrom("3s"),
+ Select: types.NullDNSSelect{DNSSelect: types.DNSfirst, Valid: true},
+ Policy: types.NullDNSPolicy{DNSPolicy: types.DNSpreferIPv4, Valid: false},
+ }}, 3,
+ },
+ }
+
+ expErr := sr(`dial tcp 127.0.0.254:HTTPBIN_PORT: connect: connection refused`)
+ if runtime.GOOS == "windows" {
+ expErr = "context deadline exceeded"
+ }
+ for name, tc := range testCases {
+ tc := tc
+ t.Run(name, func(t *testing.T) {
+ logger := logrus.New()
+ logger.SetOutput(ioutil.Discard)
+ logHook := testutils.SimpleLogrusHook{HookedLevels: []logrus.Level{logrus.WarnLevel}}
+ logger.AddHook(&logHook)
+
+ runner, err := js.New(logger, &loader.SourceData{
+ URL: &url.URL{Path: "/script.js"}, Data: []byte(script),
+ }, nil, lib.RuntimeOptions{})
+ require.NoError(t, err)
+
+ mr := mockresolver.New(nil, net.LookupIP)
+ runner.ActualResolver = mr.LookupIPAll
+
+ ctx, cancel, execScheduler, samples := newTestExecutionScheduler(t, runner, logger, tc.opts)
+ defer cancel()
+
+ mr.Set("myhost", sr("HTTPBIN_IP"))
+ time.AfterFunc(1700*time.Millisecond, func() {
+ mr.Set("myhost", "127.0.0.254")
+ })
+ defer mr.Unset("myhost")
+
+ errCh := make(chan error, 1)
+ go func() { errCh <- execScheduler.Run(ctx, ctx, samples) }()
+
+ select {
+ case err := <-errCh:
+ require.NoError(t, err)
+ entries := logHook.Drain()
+ require.Len(t, entries, tc.expLogEntries)
+ for _, entry := range entries {
+ require.IsType(t, &url.Error{}, entry.Data["error"])
+ assert.EqualError(t, entry.Data["error"].(*url.Error).Err, expErr)
+ }
+ case <-time.After(10 * time.Second):
+ t.Fatal("timed out")
+ }
+ })
+ }
+ })
+}
+
func TestRealTimeAndSetupTeardownMetrics(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip()
@@ -1100,7 +1202,10 @@ func TestRealTimeAndSetupTeardownMetrics(t *testing.T) {
getDummyTrail := func(group string, emitIterations bool, addExpTags ...string) stats.SampleContainer {
expTags := []string{"group", group}
expTags = append(expTags, addExpTags...)
- return netext.NewDialer(net.Dialer{}).GetTrail(time.Now(), time.Now(),
+ return netext.NewDialer(
+ net.Dialer{},
+ netext.NewResolver(net.LookupIP, 0, types.DNSfirst, types.DNSpreferIPv4),
+ ).GetTrail(time.Now(), time.Now(),
true, emitIterations, getTags(expTags...))
}
diff --git a/go.mod b/go.mod
index 407fbd792a7..b35b88bb09a 100644
--- a/go.mod
+++ b/go.mod
@@ -66,7 +66,6 @@ require (
github.com/urfave/negroni v0.3.1-0.20180130044549-22c5532ea862
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect
- github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8
github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7
diff --git a/go.sum b/go.sum
index 6dbf50f6750..b1301d267f7 100644
--- a/go.sum
+++ b/go.sum
@@ -153,8 +153,6 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
-github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8 h1:EVObHAr8DqpoJCVv6KYTle8FEImKhtkfcZetNqxDoJQ=
-github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE=
github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5 h1:Zs6mpwXvlqpF9zHl5XaN0p5V4J9XvP+WBuiuXyIgqvc=
github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5/go.mod h1:c1r+Ob9tUTPB0FKWO1+x+Hsc/zNa45WdGq7Y38Ybip0=
golang.org/x/crypto v0.0.0-20180308185624-c7dcf104e3a7 h1:c9Tyi4qyEZwEJ1+Zm6Fcqf+68wmUdMzfXYTp3s8Nzg8=
diff --git a/js/bundle.go b/js/bundle.go
index e0fcb5ce33c..a179dd107e0 100644
--- a/js/bundle.go
+++ b/js/bundle.go
@@ -249,7 +249,7 @@ func (b *Bundle) Instantiate(logger logrus.FieldLogger, vuID int64) (bi *BundleI
}
// Grab any exported functions that could be executed. These were
- // already pre-validated in NewBundle(), just get them here.
+ // already pre-validated in cmd.validateScenarioConfig(), just get them here.
exports := rt.Get("exports").ToObject(rt)
for k := range b.exports {
fn, _ := goja.AssertFunction(exports.Get(k))
diff --git a/js/initcontext_test.go b/js/initcontext_test.go
index e6d73c041eb..768aee5e960 100644
--- a/js/initcontext_test.go
+++ b/js/initcontext_test.go
@@ -43,6 +43,7 @@ import (
"github.com/loadimpact/k6/lib/consts"
"github.com/loadimpact/k6/lib/netext"
"github.com/loadimpact/k6/lib/testutils"
+ "github.com/loadimpact/k6/lib/types"
"github.com/loadimpact/k6/stats"
)
@@ -388,11 +389,14 @@ func TestRequestWithBinaryFile(t *testing.T) {
Logger: logger,
Group: root,
Transport: &http.Transport{
- DialContext: (netext.NewDialer(net.Dialer{
- Timeout: 10 * time.Second,
- KeepAlive: 60 * time.Second,
- DualStack: true,
- })).DialContext,
+ DialContext: (netext.NewDialer(
+ net.Dialer{
+ Timeout: 10 * time.Second,
+ KeepAlive: 60 * time.Second,
+ DualStack: true,
+ },
+ netext.NewResolver(net.LookupIP, 0, types.DNSfirst, types.DNSpreferIPv4),
+ )).DialContext,
},
BPool: bpool.NewBufferPool(1),
Samples: make(chan stats.SampleContainer, 500),
diff --git a/js/runner.go b/js/runner.go
index eb584e64bed..370ebf17790 100644
--- a/js/runner.go
+++ b/js/runner.go
@@ -36,7 +36,6 @@ import (
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/afero"
- "github.com/viki-org/dnscache"
"golang.org/x/net/http2"
"golang.org/x/time/rate"
@@ -44,6 +43,7 @@ import (
"github.com/loadimpact/k6/lib"
"github.com/loadimpact/k6/lib/consts"
"github.com/loadimpact/k6/lib/netext"
+ "github.com/loadimpact/k6/lib/types"
"github.com/loadimpact/k6/loader"
"github.com/loadimpact/k6/stats"
)
@@ -60,8 +60,10 @@ type Runner struct {
defaultGroup *lib.Group
BaseDialer net.Dialer
- Resolver *dnscache.Resolver
- RPSLimit *rate.Limiter
+ Resolver netext.Resolver
+ // TODO: Remove ActualResolver, it's a hack to simplify mocking in tests.
+ ActualResolver netext.MultiResolver
+ RPSLimit *rate.Limiter
console *console
setupData []byte
@@ -95,6 +97,7 @@ func newFromBundle(logger *logrus.Logger, b *Bundle) (*Runner, error) {
return nil, err
}
+ defDNS := types.DefaultDNSConfig()
r := &Runner{
Bundle: b,
Logger: logger,
@@ -104,8 +107,10 @@ func newFromBundle(logger *logrus.Logger, b *Bundle) (*Runner, error) {
KeepAlive: 30 * time.Second,
DualStack: true,
},
- console: newConsole(logger),
- Resolver: dnscache.New(0),
+ console: newConsole(logger),
+ Resolver: netext.NewResolver(
+ net.LookupIP, 0, defDNS.Select.DNSSelect, defDNS.Policy.DNSPolicy),
+ ActualResolver: net.LookupIP,
}
err = r.SetOptions(r.Bundle.Options)
@@ -319,9 +324,64 @@ func (r *Runner) SetOptions(opts lib.Options) error {
r.console = c
}
+ // FIXME: Resolver probably shouldn't be reset here...
+ // It's done because the js.Runner is created before the full
+ // configuration has been processed, at which point we don't have
+ // access to the DNSConfig, and need to wait for this SetOptions
+ // call that happens after all config has been assembled.
+ // We could make DNSConfig part of RuntimeOptions, but that seems
+ // conceptually wrong since the JS runtime doesn't care about it
+ // (it needs the actual resolver, not the config), and it would
+ // require an additional field on Bundle to pass the config through,
+ // which is arguably worse than this.
+ if err := r.setResolver(opts.DNS); err != nil {
+ return err
+ }
+
return nil
}
+func (r *Runner) setResolver(dns types.DNSConfig) error {
+ ttl, err := parseTTL(dns.TTL.String)
+ if err != nil {
+ return err
+ }
+
+ dnsSel := dns.Select
+ if !dnsSel.Valid {
+ dnsSel = types.DefaultDNSConfig().Select
+ }
+ dnsPol := dns.Policy
+ if !dnsPol.Valid {
+ dnsPol = types.DefaultDNSConfig().Policy
+ }
+ r.Resolver = netext.NewResolver(
+ r.ActualResolver, ttl, dnsSel.DNSSelect, dnsPol.DNSPolicy)
+
+ return nil
+}
+
+func parseTTL(ttlS string) (time.Duration, error) {
+ ttl := time.Duration(0)
+ switch ttlS {
+ case "inf":
+ // cache "infinitely"
+ ttl = time.Hour * 24 * 365
+ case "0":
+ // disable cache
+ case "":
+ ttlS = types.DefaultDNSConfig().TTL.String
+ fallthrough
+ default:
+ var err error
+ ttl, err = types.ParseExtendedDurationMs(ttlS)
+ if ttl < 0 || err != nil {
+ return ttl, fmt.Errorf("invalid DNS TTL: %s", ttlS)
+ }
+ }
+ return ttl, nil
+}
+
// Runs an exported function in its own temporary VU, optionally with an argument. Execution is
// interrupted if the context expires. No error is returned if the part does not exist.
func (r *Runner) runPart(ctx context.Context, out chan<- stats.SampleContainer, name string, arg interface{}) (goja.Value, error) {
diff --git a/lib/netext/dialer.go b/lib/netext/dialer.go
index 050c4660067..9e940e008fb 100644
--- a/lib/netext/dialer.go
+++ b/lib/netext/dialer.go
@@ -28,27 +28,18 @@ import (
"sync/atomic"
"time"
- "github.com/pkg/errors"
- "github.com/viki-org/dnscache"
-
"github.com/loadimpact/k6/lib"
"github.com/loadimpact/k6/lib/metrics"
"github.com/loadimpact/k6/lib/types"
"github.com/loadimpact/k6/stats"
)
-// dnsResolver is an interface that fetches dns information
-// about a given address.
-type dnsResolver interface {
- FetchOne(address string) (net.IP, error)
-}
-
// Dialer wraps net.Dialer and provides k6 specific functionality -
// tracing, blacklists and DNS cache and aliases.
type Dialer struct {
net.Dialer
- Resolver dnsResolver
+ Resolver Resolver
Blacklist []*lib.IPNet
BlockedHostnames *types.HostnameTrie
Hosts map[string]*lib.HostAddress
@@ -57,12 +48,8 @@ type Dialer struct {
BytesWritten int64
}
-// NewDialer constructs a new Dialer and initializes its cache.
-func NewDialer(dialer net.Dialer) *Dialer {
- return newDialerWithResolver(dialer, dnscache.New(0))
-}
-
-func newDialerWithResolver(dialer net.Dialer, resolver dnsResolver) *Dialer {
+// NewDialer constructs a new Dialer with the given DNS resolver.
+func NewDialer(dialer net.Dialer, resolver Resolver) *Dialer {
return &Dialer{
Dialer: dialer,
Resolver: resolver,
@@ -191,17 +178,13 @@ func (d *Dialer) findRemote(addr string) (*lib.HostAddress, error) {
return lib.NewHostAddress(ip, port)
}
- return d.fetchRemoteFromResolver(host, port)
-}
-
-func (d *Dialer) fetchRemoteFromResolver(host, port string) (*lib.HostAddress, error) {
- ip, err := d.Resolver.FetchOne(host)
+ ip, err = d.Resolver.LookupIP(host)
if err != nil {
return nil, err
}
if ip == nil {
- return nil, errors.Errorf("lookup %s: no such host", host)
+ return nil, fmt.Errorf("lookup %s: no such host", host)
}
return lib.NewHostAddress(ip, port)
diff --git a/lib/netext/dialer_test.go b/lib/netext/dialer_test.go
index 71db79cc07e..46125005f3c 100644
--- a/lib/netext/dialer_test.go
+++ b/lib/netext/dialer_test.go
@@ -24,19 +24,15 @@ import (
"net"
"testing"
+ "github.com/stretchr/testify/require"
+
"github.com/loadimpact/k6/lib"
+ "github.com/loadimpact/k6/lib/testutils/mockresolver"
"github.com/loadimpact/k6/lib/types"
- "github.com/stretchr/testify/require"
)
-type testResolver struct {
- hosts map[string]net.IP
-}
-
-func (r testResolver) FetchOne(host string) (net.IP, error) { return r.hosts[host], nil }
-
func TestDialerAddr(t *testing.T) {
- dialer := newDialerWithResolver(net.Dialer{}, newResolver())
+ dialer := NewDialer(net.Dialer{}, newResolver())
dialer.Hosts = map[string]*lib.HostAddress{
"example.com": {IP: net.ParseIP("3.4.5.6")},
"example.com:443": {IP: net.ParseIP("3.4.5.6"), Port: 8443},
@@ -95,7 +91,7 @@ func TestDialerAddr(t *testing.T) {
}
func TestDialerAddrBlockHostnamesStar(t *testing.T) {
- dialer := newDialerWithResolver(net.Dialer{}, newResolver())
+ dialer := NewDialer(net.Dialer{}, newResolver())
dialer.Hosts = map[string]*lib.HostAddress{
"example.com": {IP: net.ParseIP("3.4.5.6")},
}
@@ -129,12 +125,12 @@ func TestDialerAddrBlockHostnamesStar(t *testing.T) {
}
}
-func newResolver() testResolver {
- return testResolver{
- hosts: map[string]net.IP{
- "example-resolver.com": net.ParseIP("1.2.3.4"),
- "example-deny-resolver.com": net.ParseIP("8.9.10.11"),
- "example-ipv6-deny-resolver.com": net.ParseIP("::1"),
- },
- }
+func newResolver() *mockresolver.MockResolver {
+ return mockresolver.New(
+ map[string][]net.IP{
+ "example-resolver.com": {net.ParseIP("1.2.3.4")},
+ "example-deny-resolver.com": {net.ParseIP("8.9.10.11")},
+ "example-ipv6-deny-resolver.com": {net.ParseIP("::1")},
+ }, nil,
+ )
}
diff --git a/lib/netext/httpext/tracer_test.go b/lib/netext/httpext/tracer_test.go
index c2b56974180..fb97b8ea3c0 100644
--- a/lib/netext/httpext/tracer_test.go
+++ b/lib/netext/httpext/tracer_test.go
@@ -42,6 +42,7 @@ import (
"github.com/loadimpact/k6/lib/metrics"
"github.com/loadimpact/k6/lib/netext"
+ "github.com/loadimpact/k6/lib/types"
"github.com/loadimpact/k6/stats"
)
@@ -55,7 +56,10 @@ func TestTracer(t *testing.T) {
transport, ok := srv.Client().Transport.(*http.Transport)
assert.True(t, ok)
- transport.DialContext = netext.NewDialer(net.Dialer{}).DialContext
+ transport.DialContext = netext.NewDialer(
+ net.Dialer{},
+ netext.NewResolver(net.LookupIP, 0, types.DNSfirst, types.DNSpreferIPv4),
+ ).DialContext
var prev int64
assertLaterOrZero := func(t *testing.T, val int64, canBeZero bool) {
diff --git a/lib/netext/resolver.go b/lib/netext/resolver.go
new file mode 100644
index 00000000000..25795a2ef40
--- /dev/null
+++ b/lib/netext/resolver.go
@@ -0,0 +1,190 @@
+/*
+ *
+ * k6 - a next-generation load testing tool
+ * Copyright (C) 2020 Load Impact
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package netext
+
+import (
+ "math/rand"
+ "net"
+ "sync"
+ "time"
+
+ "github.com/loadimpact/k6/lib/types"
+)
+
+// MultiResolver returns all IP addresses for the given host.
+type MultiResolver func(host string) ([]net.IP, error)
+
+// Resolver is an interface that returns DNS information about a given host.
+type Resolver interface {
+ LookupIP(host string) (net.IP, error)
+}
+
+type resolver struct {
+ resolve MultiResolver
+ selectIndex types.DNSSelect
+ policy types.DNSPolicy
+ rrm *sync.Mutex
+ rand *rand.Rand
+ roundRobin map[string]uint8
+}
+
+type cacheRecord struct {
+ ips []net.IP
+ lastLookup time.Time
+}
+
+type cacheResolver struct {
+ resolver
+ ttl time.Duration
+ cm *sync.Mutex
+ cache map[string]cacheRecord
+}
+
+// NewResolver returns a new DNS resolver. If ttl is not 0, responses
+// will be cached per host for the specified period. The IP returned from
+// LookupIP() will be selected based on the given sel and pol values.
+func NewResolver(
+ actRes MultiResolver, ttl time.Duration, sel types.DNSSelect, pol types.DNSPolicy,
+) Resolver {
+ r := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint: gosec
+ res := resolver{
+ resolve: actRes,
+ selectIndex: sel,
+ policy: pol,
+ rrm: &sync.Mutex{},
+ rand: r,
+ roundRobin: make(map[string]uint8),
+ }
+ if ttl == 0 {
+ return &res
+ }
+ return &cacheResolver{
+ resolver: res,
+ ttl: ttl,
+ cm: &sync.Mutex{},
+ cache: make(map[string]cacheRecord),
+ }
+}
+
+// LookupIP returns a single IP resolved for host, selected according to the
+// configured select and policy options.
+func (r *resolver) LookupIP(host string) (net.IP, error) {
+ ips, err := r.resolve(host)
+ if err != nil {
+ return nil, err
+ }
+
+ ips = r.applyPolicy(ips)
+ return r.selectOne(host, ips), nil
+}
+
+// LookupIP returns a single IP resolved for host, selected according to the
+// configured select and policy options. Results are cached per host and will be
+// refreshed if the last lookup time exceeds the configured TTL (not the TTL
+// returned in the DNS record).
+func (r *cacheResolver) LookupIP(host string) (net.IP, error) {
+ r.cm.Lock()
+
+ var ips []net.IP
+ // TODO: Invalidate? When?
+ if cr, ok := r.cache[host]; ok && time.Now().Before(cr.lastLookup.Add(r.ttl)) {
+ ips = cr.ips
+ } else {
+ r.cm.Unlock() // The lookup could take some time, so unlock momentarily.
+ var err error
+ ips, err = r.resolve(host)
+ if err != nil {
+ return nil, err
+ }
+ ips = r.applyPolicy(ips)
+ r.cm.Lock()
+ r.cache[host] = cacheRecord{ips: ips, lastLookup: time.Now()}
+ }
+
+ r.cm.Unlock()
+
+ return r.selectOne(host, ips), nil
+}
+
+func (r *resolver) selectOne(host string, ips []net.IP) net.IP {
+ if len(ips) == 0 {
+ return nil
+ }
+
+ var ip net.IP
+ switch r.selectIndex {
+ case types.DNSfirst:
+ return ips[0]
+ case types.DNSroundRobin:
+ r.rrm.Lock()
+ // NOTE: This index approach is not stable and might result in returning
+ // repeated or skipped IPs if the records change during a test run.
+ ip = ips[int(r.roundRobin[host])%len(ips)]
+ r.roundRobin[host]++
+ r.rrm.Unlock()
+ case types.DNSrandom:
+ r.rrm.Lock()
+ ip = ips[r.rand.Intn(len(ips))]
+ r.rrm.Unlock()
+ }
+
+ return ip
+}
+
+func (r *resolver) applyPolicy(ips []net.IP) (retIPs []net.IP) {
+ if r.policy == types.DNSany {
+ return ips
+ }
+ ip4, ip6 := groupByVersion(ips)
+ switch r.policy {
+ case types.DNSpreferIPv4:
+ retIPs = ip4
+ if len(retIPs) == 0 {
+ retIPs = ip6
+ }
+ case types.DNSpreferIPv6:
+ retIPs = ip6
+ if len(retIPs) == 0 {
+ retIPs = ip4
+ }
+ case types.DNSonlyIPv4:
+ retIPs = ip4
+ case types.DNSonlyIPv6:
+ retIPs = ip6
+ // Already checked above, but added to satisfy 'exhaustive' linter.
+ case types.DNSany:
+ retIPs = ips
+ }
+
+ return
+}
+
+func groupByVersion(ips []net.IP) (ip4 []net.IP, ip6 []net.IP) {
+ for _, ip := range ips {
+ if ip.To4() != nil {
+ ip4 = append(ip4, ip)
+ } else {
+ ip6 = append(ip6, ip)
+ }
+ }
+
+ return
+}
diff --git a/lib/netext/resolver_test.go b/lib/netext/resolver_test.go
new file mode 100644
index 00000000000..4ec75c2f87c
--- /dev/null
+++ b/lib/netext/resolver_test.go
@@ -0,0 +1,116 @@
+/*
+ *
+ * k6 - a next-generation load testing tool
+ * Copyright (C) 2020 Load Impact
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package netext
+
+import (
+ "fmt"
+ "net"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/loadimpact/k6/lib/testutils/mockresolver"
+ "github.com/loadimpact/k6/lib/types"
+)
+
+func TestResolver(t *testing.T) {
+ t.Parallel()
+
+ host := "myhost"
+ mr := mockresolver.New(map[string][]net.IP{
+ host: {
+ net.ParseIP("127.0.0.10"),
+ net.ParseIP("127.0.0.11"),
+ net.ParseIP("127.0.0.12"),
+ net.ParseIP("2001:db8::10"),
+ net.ParseIP("2001:db8::11"),
+ net.ParseIP("2001:db8::12"),
+ },
+ }, nil)
+
+ t.Run("LookupIP", func(t *testing.T) {
+ testCases := []struct {
+ ttl time.Duration
+ sel types.DNSSelect
+ pol types.DNSPolicy
+ expIP []net.IP
+ }{
+ {
+ 0, types.DNSfirst, types.DNSpreferIPv4,
+ []net.IP{net.ParseIP("127.0.0.10")},
+ },
+ {
+ time.Second, types.DNSfirst, types.DNSpreferIPv4,
+ []net.IP{net.ParseIP("127.0.0.10")},
+ },
+ {0, types.DNSroundRobin, types.DNSonlyIPv6, []net.IP{
+ net.ParseIP("2001:db8::10"),
+ net.ParseIP("2001:db8::11"),
+ net.ParseIP("2001:db8::12"),
+ net.ParseIP("2001:db8::10"),
+ }},
+ {
+ 0, types.DNSfirst, types.DNSpreferIPv6,
+ []net.IP{net.ParseIP("2001:db8::10")},
+ },
+ {0, types.DNSroundRobin, types.DNSpreferIPv4, []net.IP{
+ net.ParseIP("127.0.0.10"),
+ net.ParseIP("127.0.0.11"),
+ net.ParseIP("127.0.0.12"),
+ net.ParseIP("127.0.0.10"),
+ }},
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(fmt.Sprintf("%s_%s_%s", tc.ttl, tc.sel, tc.pol), func(t *testing.T) {
+ r := NewResolver(mr.LookupIPAll, tc.ttl, tc.sel, tc.pol)
+ ip, err := r.LookupIP(host)
+ require.NoError(t, err)
+ assert.Equal(t, tc.expIP[0], ip)
+
+ if tc.ttl > 0 {
+ require.IsType(t, &cacheResolver{}, r)
+ cr := r.(*cacheResolver)
+ assert.Len(t, cr.cache, 1)
+ assert.Equal(t, tc.ttl, cr.ttl)
+ firstLookup := cr.cache[host].lastLookup
+ time.Sleep(cr.ttl + 100*time.Millisecond)
+ _, err = r.LookupIP(host)
+ require.NoError(t, err)
+ assert.True(t, cr.cache[host].lastLookup.After(firstLookup))
+ }
+
+ if tc.sel == types.DNSroundRobin {
+ ips := []net.IP{ip}
+ for i := 0; i < 3; i++ {
+ ip, err = r.LookupIP(host)
+ require.NoError(t, err)
+ ips = append(ips, ip)
+ }
+ assert.Equal(t, tc.expIP, ips)
+ }
+ })
+ }
+ })
+}
diff --git a/lib/options.go b/lib/options.go
index 20555d4fd41..6dfec6ed4c7 100644
--- a/lib/options.go
+++ b/lib/options.go
@@ -307,6 +307,9 @@ type Options struct {
// Limit HTTP requests per second.
RPS null.Int `json:"rps" envconfig:"K6_RPS"`
+ // DNS handling configuration.
+ DNS types.DNSConfig `json:"dns" envconfig:"K6_DNS"`
+
// How many HTTP redirects do we follow?
MaxRedirects null.Int `json:"maxRedirects" envconfig:"K6_MAX_REDIRECTS"`
@@ -539,6 +542,15 @@ func (o Options) Apply(opts Options) Options {
if opts.ConsoleOutput.Valid {
o.ConsoleOutput = opts.ConsoleOutput
}
+ if opts.DNS.TTL.Valid {
+ o.DNS.TTL = opts.DNS.TTL
+ }
+ if opts.DNS.Select.Valid {
+ o.DNS.Select = opts.DNS.Select
+ }
+ if opts.DNS.Policy.Valid {
+ o.DNS.Policy = opts.DNS.Policy
+ }
return o
}
diff --git a/lib/testutils/httpmultibin/httpmultibin.go b/lib/testutils/httpmultibin/httpmultibin.go
index dfb3a741975..71c2cb93768 100644
--- a/lib/testutils/httpmultibin/httpmultibin.go
+++ b/lib/testutils/httpmultibin/httpmultibin.go
@@ -49,6 +49,7 @@ import (
"github.com/loadimpact/k6/lib"
"github.com/loadimpact/k6/lib/netext"
"github.com/loadimpact/k6/lib/netext/httpext"
+ "github.com/loadimpact/k6/lib/types"
)
// GetTLSClientConfig returns a TLS config that trusts the supplied
@@ -253,7 +254,7 @@ func NewHTTPMultiBin(t testing.TB) *HTTPMultiBin {
Timeout: 2 * time.Second,
KeepAlive: 10 * time.Second,
DualStack: true,
- })
+ }, netext.NewResolver(net.LookupIP, 0, types.DNSfirst, types.DNSpreferIPv4))
dialer.Hosts = map[string]*lib.HostAddress{
httpDomain: httpDomainValue,
httpsDomain: httpsDomainValue,
diff --git a/lib/testutils/mockresolver/resolver.go b/lib/testutils/mockresolver/resolver.go
new file mode 100644
index 00000000000..5e68d260893
--- /dev/null
+++ b/lib/testutils/mockresolver/resolver.go
@@ -0,0 +1,81 @@
+/*
+ *
+ * k6 - a next-generation load testing tool
+ * Copyright (C) 2020 Load Impact
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package mockresolver
+
+import (
+ "fmt"
+ "net"
+ "sync"
+)
+
+// MockResolver implements netext.Resolver, and allows changing the host
+// mapping at runtime.
+type MockResolver struct {
+ m sync.RWMutex
+ hosts map[string][]net.IP
+ fallback func(host string) ([]net.IP, error)
+}
+
+// New returns a new MockResolver.
+func New(hosts map[string][]net.IP, fallback func(host string) ([]net.IP, error)) *MockResolver {
+ if hosts == nil {
+ hosts = make(map[string][]net.IP)
+ }
+ return &MockResolver{hosts: hosts, fallback: fallback}
+}
+
+// LookupIP returns the first IP mapped for host.
+func (r *MockResolver) LookupIP(host string) (net.IP, error) {
+ if ips, err := r.LookupIPAll(host); err != nil {
+ return nil, err
+ } else if len(ips) > 0 {
+ return ips[0], nil
+ }
+ return nil, nil
+}
+
+// LookupIPAll returns all IPs mapped for host. It mimics the net.LookupIP
+// signature so that it can be used to mock netext.LookupIP in tests.
+func (r *MockResolver) LookupIPAll(host string) ([]net.IP, error) {
+ r.m.RLock()
+ defer r.m.RUnlock()
+ if ips, ok := r.hosts[host]; ok {
+ return ips, nil
+ }
+ if r.fallback != nil {
+ return r.fallback(host)
+ }
+ return nil, fmt.Errorf("lookup %s: no such host", host)
+}
+
+// Set the host to resolve to ip.
+func (r *MockResolver) Set(host, ip string) {
+ r.m.Lock()
+ defer r.m.Unlock()
+ r.hosts[host] = []net.IP{net.ParseIP(ip)}
+}
+
+// Unset removes the host.
+func (r *MockResolver) Unset(host string) {
+ r.m.Lock()
+ defer r.m.Unlock()
+ delete(r.hosts, host)
+}
diff --git a/lib/types/dns.go b/lib/types/dns.go
new file mode 100644
index 00000000000..adcca440c5f
--- /dev/null
+++ b/lib/types/dns.go
@@ -0,0 +1,248 @@
+/*
+ *
+ * k6 - a next-generation load testing tool
+ * Copyright (C) 2020 Load Impact
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package types
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+
+ "github.com/kubernetes/helm/pkg/strvals"
+ "gopkg.in/guregu/null.v3"
+)
+
+// DNSConfig is the DNS resolver configuration.
+type DNSConfig struct {
+ // If positive, defines how long DNS lookups should be returned from the cache.
+ TTL null.String `json:"ttl"`
+ // Select specifies the strategy to use when picking a single IP if more than one is returned for a host name.
+ Select NullDNSSelect `json:"select"`
+ // Policy specifies how to handle returning of IPv4 or IPv6 addresses.
+ Policy NullDNSPolicy `json:"policy"`
+ // FIXME: Valid is unused and is only added to satisfy some logic in
+ // lib.Options.ForEachSpecified(), otherwise it would panic with
+ // `reflect: call of reflect.Value.Bool on zero Value`.
+ Valid bool `json:"-"`
+}
+
+// DefaultDNSConfig returns the default DNS configuration.
+func DefaultDNSConfig() DNSConfig {
+ return DNSConfig{
+ TTL: null.NewString("5m", false),
+ Select: NullDNSSelect{DNSrandom, false},
+ Policy: NullDNSPolicy{DNSpreferIPv4, false},
+ }
+}
+
+// DNSPolicy specifies the preference for handling IP versions in DNS resolutions.
+//go:generate enumer -type=DNSPolicy -trimprefix DNS -output dns_policy_gen.go
+type DNSPolicy uint8
+
+// These are lower camel cased since enumer doesn't support it as a transform option.
+// See https://github.com/alvaroloes/enumer/pull/60 .
+const (
+ // DNSpreferIPv4 returns an IPv4 address if available, falling back to IPv6 otherwise.
+ DNSpreferIPv4 DNSPolicy = iota + 1
+ // DNSpreferIPv6 returns an IPv6 address if available, falling back to IPv4 otherwise.
+ DNSpreferIPv6
+ // DNSonlyIPv4 only returns an IPv4 address and the resolution will fail if no IPv4 address is found.
+ DNSonlyIPv4
+ // DNSonlyIPv6 only returns an IPv6 address and the resolution will fail if no IPv6 address is found.
+ DNSonlyIPv6
+ // DNSany returns any resolved address regardless of version.
+ DNSany
+)
+
+// UnmarshalJSON converts JSON data to a valid DNSPolicy
+func (d *DNSPolicy) UnmarshalJSON(data []byte) error {
+ if bytes.Equal(data, []byte(`null`)) {
+ return nil
+ }
+ var s string
+ if err := json.Unmarshal(data, &s); err != nil {
+ return err
+ }
+ v, err := DNSPolicyString(s)
+ if err != nil {
+ return err
+ }
+ *d = v
+ return nil
+}
+
+// MarshalJSON returns the JSON representation of d.
+func (d DNSPolicy) MarshalJSON() ([]byte, error) {
+ return json.Marshal(d.String())
+}
+
+// NullDNSPolicy is a nullable wrapper around DNSPolicy, required for the
+// current configuration system.
+type NullDNSPolicy struct {
+ DNSPolicy
+ Valid bool
+}
+
+// UnmarshalJSON converts JSON data to a valid NullDNSPolicy.
+func (d *NullDNSPolicy) UnmarshalJSON(data []byte) error {
+ if bytes.Equal(data, []byte(`null`)) {
+ return nil
+ }
+ if err := json.Unmarshal(data, &d.DNSPolicy); err != nil {
+ return err
+ }
+ d.Valid = true
+ return nil
+}
+
+// MarshalJSON returns the JSON representation of d.
+func (d NullDNSPolicy) MarshalJSON() ([]byte, error) {
+ if !d.Valid {
+ return []byte(`null`), nil
+ }
+ return json.Marshal(d.DNSPolicy)
+}
+
+// DNSSelect is the strategy to use when picking a single IP if more than one
+// is returned for a host name.
+//go:generate enumer -type=DNSSelect -trimprefix DNS -output dns_select_gen.go
+type DNSSelect uint8
+
+// These are lower camel cased since enumer doesn't support it as a transform option.
+// See https://github.com/alvaroloes/enumer/pull/60 .
+const (
+ // DNSfirst returns the first IP from the response.
+ DNSfirst DNSSelect = iota + 1
+ // DNSroundRobin rotates the IP returned on each lookup.
+ DNSroundRobin
+ // DNSrandom returns a random IP from the response.
+ DNSrandom
+)
+
+// UnmarshalJSON converts JSON data to a valid DNSSelect
+func (d *DNSSelect) UnmarshalJSON(data []byte) error {
+ if bytes.Equal(data, []byte(`null`)) {
+ return nil
+ }
+ var s string
+ if err := json.Unmarshal(data, &s); err != nil {
+ return err
+ }
+ v, err := DNSSelectString(s)
+ if err != nil {
+ return err
+ }
+ *d = v
+ return nil
+}
+
+// MarshalJSON returns the JSON representation of d.
+func (d DNSSelect) MarshalJSON() ([]byte, error) {
+ return json.Marshal(d.String())
+}
+
+// NullDNSSelect is a nullable wrapper around DNSSelect, required for the
+// current configuration system.
+type NullDNSSelect struct {
+ DNSSelect
+ Valid bool
+}
+
+// UnmarshalJSON converts JSON data to a valid NullDNSSelect.
+func (d *NullDNSSelect) UnmarshalJSON(data []byte) error {
+ if bytes.Equal(data, []byte(`null`)) {
+ return nil
+ }
+ if err := json.Unmarshal(data, &d.DNSSelect); err != nil {
+ return err
+ }
+ d.Valid = true
+ return nil
+}
+
+// MarshalJSON returns the JSON representation of d.
+func (d NullDNSSelect) MarshalJSON() ([]byte, error) {
+ if !d.Valid {
+ return []byte(`null`), nil
+ }
+ return json.Marshal(d.DNSSelect)
+}
+
+// String implements fmt.Stringer.
+func (c DNSConfig) String() string {
+ return fmt.Sprintf("ttl=%s,select=%s,policy=%s",
+ c.TTL.String, c.Select.String(), c.Policy.String())
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (c *DNSConfig) UnmarshalJSON(data []byte) error {
+ var s struct {
+ TTL null.String `json:"ttl"`
+ Select NullDNSSelect `json:"select"`
+ Policy NullDNSPolicy `json:"policy"`
+ }
+ if err := json.Unmarshal(data, &s); err != nil {
+ return err
+ }
+ c.TTL = s.TTL
+ c.Select = s.Select
+ c.Policy = s.Policy
+ return nil
+}
+
+// UnmarshalText implements encoding.TextUnmarshaler.
+func (c *DNSConfig) UnmarshalText(text []byte) error {
+ if string(text) == DefaultDNSConfig().String() {
+ *c = DefaultDNSConfig()
+ return nil
+ }
+ params, err := strvals.Parse(string(text))
+ if err != nil {
+ return err
+ }
+ return c.unmarshal(params)
+}
+
+func (c *DNSConfig) unmarshal(params map[string]interface{}) error {
+ for k, v := range params {
+ switch k {
+ case "policy":
+ p, err := DNSPolicyString(v.(string))
+ if err != nil {
+ return err
+ }
+ c.Policy.DNSPolicy = p
+ c.Policy.Valid = true
+ case "select":
+ s, err := DNSSelectString(v.(string))
+ if err != nil {
+ return err
+ }
+ c.Select.DNSSelect = s
+ c.Select.Valid = true
+ case "ttl":
+ ttlv := fmt.Sprintf("%v", v)
+ c.TTL = null.StringFrom(ttlv)
+ default:
+ return fmt.Errorf("unknown DNS configuration field: %s", k)
+ }
+ }
+ return nil
+}
diff --git a/lib/types/dns_policy_gen.go b/lib/types/dns_policy_gen.go
new file mode 100644
index 00000000000..85feb6d24a7
--- /dev/null
+++ b/lib/types/dns_policy_gen.go
@@ -0,0 +1,54 @@
+// Code generated by "enumer -type=DNSPolicy -trimprefix DNS -output dns_policy_gen.go"; DO NOT EDIT.
+
+//
+package types
+
+import (
+ "fmt"
+)
+
+const _DNSPolicyName = "preferIPv4preferIPv6onlyIPv4onlyIPv6any"
+
+var _DNSPolicyIndex = [...]uint8{0, 10, 20, 28, 36, 39}
+
+func (i DNSPolicy) String() string {
+ i -= 1
+ if i >= DNSPolicy(len(_DNSPolicyIndex)-1) {
+ return fmt.Sprintf("DNSPolicy(%d)", i+1)
+ }
+ return _DNSPolicyName[_DNSPolicyIndex[i]:_DNSPolicyIndex[i+1]]
+}
+
+var _DNSPolicyValues = []DNSPolicy{1, 2, 3, 4, 5}
+
+var _DNSPolicyNameToValueMap = map[string]DNSPolicy{
+ _DNSPolicyName[0:10]: 1,
+ _DNSPolicyName[10:20]: 2,
+ _DNSPolicyName[20:28]: 3,
+ _DNSPolicyName[28:36]: 4,
+ _DNSPolicyName[36:39]: 5,
+}
+
+// DNSPolicyString retrieves an enum value from the enum constants string name.
+// Throws an error if the param is not part of the enum.
+func DNSPolicyString(s string) (DNSPolicy, error) {
+ if val, ok := _DNSPolicyNameToValueMap[s]; ok {
+ return val, nil
+ }
+ return 0, fmt.Errorf("%s does not belong to DNSPolicy values", s)
+}
+
+// DNSPolicyValues returns all values of the enum
+func DNSPolicyValues() []DNSPolicy {
+ return _DNSPolicyValues
+}
+
+// IsADNSPolicy returns "true" if the value is listed in the enum definition. "false" otherwise
+func (i DNSPolicy) IsADNSPolicy() bool {
+ for _, v := range _DNSPolicyValues {
+ if i == v {
+ return true
+ }
+ }
+ return false
+}
diff --git a/lib/types/dns_select_gen.go b/lib/types/dns_select_gen.go
new file mode 100644
index 00000000000..347aaec7a61
--- /dev/null
+++ b/lib/types/dns_select_gen.go
@@ -0,0 +1,52 @@
+// Code generated by "enumer -type=DNSSelect -trimprefix DNS -output dns_select_gen.go"; DO NOT EDIT.
+
+//
+package types
+
+import (
+ "fmt"
+)
+
+const _DNSSelectName = "firstroundRobinrandom"
+
+var _DNSSelectIndex = [...]uint8{0, 5, 15, 21}
+
+func (i DNSSelect) String() string {
+ i -= 1
+ if i >= DNSSelect(len(_DNSSelectIndex)-1) {
+ return fmt.Sprintf("DNSSelect(%d)", i+1)
+ }
+ return _DNSSelectName[_DNSSelectIndex[i]:_DNSSelectIndex[i+1]]
+}
+
+var _DNSSelectValues = []DNSSelect{1, 2, 3}
+
+var _DNSSelectNameToValueMap = map[string]DNSSelect{
+ _DNSSelectName[0:5]: 1,
+ _DNSSelectName[5:15]: 2,
+ _DNSSelectName[15:21]: 3,
+}
+
+// DNSSelectString retrieves an enum value from the enum constants string name.
+// Throws an error if the param is not part of the enum.
+func DNSSelectString(s string) (DNSSelect, error) {
+ if val, ok := _DNSSelectNameToValueMap[s]; ok {
+ return val, nil
+ }
+ return 0, fmt.Errorf("%s does not belong to DNSSelect values", s)
+}
+
+// DNSSelectValues returns all values of the enum
+func DNSSelectValues() []DNSSelect {
+ return _DNSSelectValues
+}
+
+// IsADNSSelect returns "true" if the value is listed in the enum definition. "false" otherwise
+func (i DNSSelect) IsADNSSelect() bool {
+ for _, v := range _DNSSelectValues {
+ if i == v {
+ return true
+ }
+ }
+ return false
+}
diff --git a/lib/types/types.go b/lib/types/types.go
index e9ffeab037f..03609850e59 100644
--- a/lib/types/types.go
+++ b/lib/types/types.go
@@ -123,6 +123,16 @@ func ParseExtendedDuration(data string) (result time.Duration, err error) {
return time.Duration(days)*24*time.Hour + hours, nil
}
+// ParseExtendedDurationMs wraps ParseExtendedDuration while assuming
+// millisecond values if data is provided with no units.
+// TODO: Merge this into ParseExtendedDuration once it's safe to do so globally.
+func ParseExtendedDurationMs(data string) (result time.Duration, err error) {
+ if t, errp := strconv.ParseFloat(data, 32); errp == nil {
+ data = fmt.Sprintf("%.2fms", t)
+ }
+ return ParseExtendedDuration(data)
+}
+
// UnmarshalText converts text data to Duration
func (d *Duration) UnmarshalText(data []byte) error {
v, err := ParseExtendedDuration(string(data))
diff --git a/vendor/github.com/viki-org/dnscache/dnscache.go b/vendor/github.com/viki-org/dnscache/dnscache.go
deleted file mode 100644
index 74d6bd61fcb..00000000000
--- a/vendor/github.com/viki-org/dnscache/dnscache.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package dnscache
-// Package dnscache caches DNS lookups
-
-import (
- "net"
- "sync"
- "time"
-)
-
-type Resolver struct {
- lock sync.RWMutex
- cache map[string][]net.IP
-}
-
-func New(refreshRate time.Duration) *Resolver {
- resolver := &Resolver {
- cache: make(map[string][]net.IP, 64),
- }
- if refreshRate > 0 {
- go resolver.autoRefresh(refreshRate)
- }
- return resolver
-}
-
-func (r *Resolver) Fetch(address string) ([]net.IP, error) {
- r.lock.RLock()
- ips, exists := r.cache[address]
- r.lock.RUnlock()
- if exists { return ips, nil }
-
- return r.Lookup(address)
-}
-
-func (r *Resolver) FetchOne(address string) (net.IP, error) {
- ips, err := r.Fetch(address)
- if err != nil || len(ips) == 0 { return nil, err}
- return ips[0], nil
-}
-
-func (r *Resolver) FetchOneString(address string) (string, error) {
- ip, err := r.FetchOne(address)
- if err != nil || ip == nil { return "", err }
- return ip.String(), nil
-}
-
-func (r *Resolver) Refresh() {
- i := 0
- r.lock.RLock()
- addresses := make([]string, len(r.cache))
- for key, _ := range r.cache {
- addresses[i] = key
- i++
- }
- r.lock.RUnlock()
-
- for _, address := range addresses {
- r.Lookup(address)
- time.Sleep(time.Second * 2)
- }
-}
-
-func (r *Resolver) Lookup(address string) ([]net.IP, error) {
- ips, err := net.LookupIP(address)
- if err != nil { return nil, err }
-
- r.lock.Lock()
- r.cache[address] = ips
- r.lock.Unlock()
- return ips, nil
-}
-
-func (r *Resolver) autoRefresh(rate time.Duration) {
- for {
- time.Sleep(rate)
- r.Refresh()
- }
-}
diff --git a/vendor/github.com/viki-org/dnscache/license.txt b/vendor/github.com/viki-org/dnscache/license.txt
deleted file mode 100644
index 8a7d969ed49..00000000000
--- a/vendor/github.com/viki-org/dnscache/license.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-Copyright (c) 2013 Viki Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
diff --git a/vendor/github.com/viki-org/dnscache/readme.md b/vendor/github.com/viki-org/dnscache/readme.md
deleted file mode 100644
index 8c737aac35d..00000000000
--- a/vendor/github.com/viki-org/dnscache/readme.md
+++ /dev/null
@@ -1,38 +0,0 @@
-### A DNS cache for Go
-CGO is used to lookup domain names. Given enough concurrent requests and the slightest hiccup in name resolution, it's quite easy to end up with blocked/leaking goroutines.
-
-The issue is documented at
-
-The Go team's singleflight solution (which isn't in stable yet) is rather elegant. However, it only eliminates concurrent lookups (thundering herd problems). Many systems can live with slightly stale resolve names, which means we can cacne DNS lookups and refresh them in the background.
-
-### Installation
-Install using the "go get" command:
-
- go get github.com/viki-org/dnscache
-
-### Usage
-The cache is thread safe. Create a new instance by specifying how long each entry should be cached (in seconds). Items will be refreshed in the background.
-
- //refresh items every 5 minutes
- resolver := dnscache.New(time.Minute * 5)
-
- //get an array of net.IP
- ips, _ := resolver.Fetch("api.viki.io")
-
- //get the first net.IP
- ip, _ := resolver.FetchOne("api.viki.io")
-
- //get the first net.IP as string
- ip, _ := resolver.FetchOneString("api.viki.io")
-
-If you are using an `http.Transport`, you can use this cache by speficifying a
-`Dial` function:
-
- transport := &http.Transport {
- MaxIdleConnsPerHost: 64,
- Dial: func(network string, address string) (net.Conn, error) {
- separator := strings.LastIndex(address, ":")
- ip, _ := dnscache.FetchString(address[:separator])
- return net.Dial("tcp", ip + address[separator:])
- },
- }
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 8c60f54c7e4..b1088edaf6b 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -225,9 +225,6 @@ github.com/valyala/bytebufferpool
# github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4
## explicit
github.com/valyala/fasttemplate
-# github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8
-## explicit
-github.com/viki-org/dnscache
# github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5
## explicit
github.com/zyedidia/highlight