386ec3a2a2
Instead it has an interface which can be mocked for better unit testing that is deterministic and not prone to flakiness.
399 lines
11 KiB
Go
399 lines
11 KiB
Go
package autoconf
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/consul/agent/agentpb"
|
|
pbconfig "github.com/hashicorp/consul/agent/agentpb/config"
|
|
"github.com/hashicorp/consul/agent/config"
|
|
"github.com/hashicorp/consul/lib"
|
|
"github.com/hashicorp/consul/sdk/testutil"
|
|
"github.com/hashicorp/consul/tlsutil"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type mockDirectRPC struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *mockDirectRPC) RPC(dc string, node string, addr net.Addr, method string, args interface{}, reply interface{}) error {
|
|
retValues := m.Called(dc, node, addr, method, args, reply)
|
|
switch ret := retValues.Get(0).(type) {
|
|
case error:
|
|
return ret
|
|
case func(interface{}):
|
|
ret(reply)
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("This should not happen, update mock direct rpc expectations")
|
|
}
|
|
}
|
|
|
|
func TestNew(t *testing.T) {
|
|
type testCase struct {
|
|
opts []Option
|
|
err string
|
|
validate func(t *testing.T, ac *AutoConfig)
|
|
}
|
|
|
|
cases := map[string]testCase{
|
|
"no-direct-rpc": {
|
|
opts: []Option{
|
|
WithTLSConfigurator(&tlsutil.Configurator{}),
|
|
},
|
|
err: "must provide a direct RPC delegate",
|
|
},
|
|
"no-tls-configurator": {
|
|
opts: []Option{
|
|
WithDirectRPC(&mockDirectRPC{}),
|
|
},
|
|
err: "must provide a TLS configurator",
|
|
},
|
|
"ok": {
|
|
opts: []Option{
|
|
WithTLSConfigurator(&tlsutil.Configurator{}),
|
|
WithDirectRPC(&mockDirectRPC{}),
|
|
},
|
|
validate: func(t *testing.T, ac *AutoConfig) {
|
|
t.Helper()
|
|
require.NotNil(t, ac.logger)
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tcase := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
ac, err := New(tcase.opts...)
|
|
if tcase.err != "" {
|
|
testutil.RequireErrorContains(t, err, tcase.err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ac)
|
|
if tcase.validate != nil {
|
|
tcase.validate(t, ac)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadConfig(t *testing.T) {
|
|
// Basically just testing that injection of the extra
|
|
// source works.
|
|
devMode := true
|
|
builderOpts := config.BuilderOpts{
|
|
// putting this in dev mode so that the config validates
|
|
// without having to specify a data directory
|
|
DevMode: &devMode,
|
|
}
|
|
|
|
cfg, warnings, err := LoadConfig(builderOpts, config.Source{
|
|
Name: "test",
|
|
Format: "hcl",
|
|
Data: `node_name = "hobbiton"`,
|
|
},
|
|
config.Source{
|
|
Name: "overrides",
|
|
Format: "json",
|
|
Data: `{"check_reap_interval": "1ms"}`,
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
require.Empty(t, warnings)
|
|
require.NotNil(t, cfg)
|
|
require.Equal(t, "hobbiton", cfg.NodeName)
|
|
require.Equal(t, 1*time.Millisecond, cfg.CheckReapInterval)
|
|
}
|
|
|
|
func TestReadConfig(t *testing.T) {
|
|
// just testing that some auto config source gets injected
|
|
devMode := true
|
|
ac := AutoConfig{
|
|
autoConfigData: `{"node_name": "hobbiton"}`,
|
|
builderOpts: config.BuilderOpts{
|
|
// putting this in dev mode so that the config validates
|
|
// without having to specify a data directory
|
|
DevMode: &devMode,
|
|
},
|
|
logger: testutil.Logger(t),
|
|
}
|
|
|
|
cfg, err := ac.ReadConfig()
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cfg)
|
|
require.Equal(t, "hobbiton", cfg.NodeName)
|
|
require.Same(t, ac.config, cfg)
|
|
}
|
|
|
|
func testSetupAutoConf(t *testing.T) (string, string, config.BuilderOpts) {
|
|
t.Helper()
|
|
|
|
// create top level directory to hold both config and data
|
|
tld := testutil.TempDir(t, "auto-config")
|
|
t.Cleanup(func() { os.RemoveAll(tld) })
|
|
|
|
// create the data directory
|
|
dataDir := filepath.Join(tld, "data")
|
|
require.NoError(t, os.Mkdir(dataDir, 0700))
|
|
|
|
// create the config directory
|
|
configDir := filepath.Join(tld, "config")
|
|
require.NoError(t, os.Mkdir(configDir, 0700))
|
|
|
|
builderOpts := config.BuilderOpts{
|
|
HCL: []string{
|
|
`data_dir = "` + dataDir + `"`,
|
|
`datacenter = "dc1"`,
|
|
`node_name = "autoconf"`,
|
|
`bind_addr = "127.0.0.1"`,
|
|
},
|
|
}
|
|
|
|
return dataDir, configDir, builderOpts
|
|
}
|
|
|
|
func TestInitialConfiguration_disabled(t *testing.T) {
|
|
dataDir, configDir, builderOpts := testSetupAutoConf(t)
|
|
|
|
cfgFile := filepath.Join(configDir, "test.json")
|
|
require.NoError(t, ioutil.WriteFile(cfgFile, []byte(`{
|
|
"primary_datacenter": "primary",
|
|
"auto_config": {"enabled": false}
|
|
}`), 0600))
|
|
|
|
builderOpts.ConfigFiles = append(builderOpts.ConfigFiles, cfgFile)
|
|
|
|
directRPC := mockDirectRPC{}
|
|
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ac)
|
|
|
|
cfg, err := ac.InitialConfiguration(context.Background())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cfg)
|
|
require.Equal(t, "primary", cfg.PrimaryDatacenter)
|
|
require.NoFileExists(t, filepath.Join(dataDir, autoConfigFileName))
|
|
|
|
// ensure no RPC was made
|
|
directRPC.AssertExpectations(t)
|
|
}
|
|
|
|
func TestInitialConfiguration_cancelled(t *testing.T) {
|
|
_, configDir, builderOpts := testSetupAutoConf(t)
|
|
|
|
cfgFile := filepath.Join(configDir, "test.json")
|
|
require.NoError(t, ioutil.WriteFile(cfgFile, []byte(`{
|
|
"primary_datacenter": "primary",
|
|
"auto_config": {"enabled": true, "intro_token": "blarg", "server_addresses": ["127.0.0.1:8300"]},
|
|
"verify_outgoing": true
|
|
}`), 0600))
|
|
|
|
builderOpts.ConfigFiles = append(builderOpts.ConfigFiles, cfgFile)
|
|
|
|
directRPC := mockDirectRPC{}
|
|
|
|
expectedRequest := agentpb.AutoConfigRequest{
|
|
Datacenter: "dc1",
|
|
Node: "autoconf",
|
|
JWT: "blarg",
|
|
}
|
|
|
|
directRPC.On("RPC", "dc1", "autoconf", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8300}, "AutoConfig.InitialConfiguration", &expectedRequest, mock.Anything).Return(fmt.Errorf("injected error")).Times(0)
|
|
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ac)
|
|
|
|
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(100*time.Millisecond))
|
|
defer cancelFn()
|
|
|
|
cfg, err := ac.InitialConfiguration(ctx)
|
|
testutil.RequireErrorContains(t, err, context.DeadlineExceeded.Error())
|
|
require.Nil(t, cfg)
|
|
|
|
// ensure no RPC was made
|
|
directRPC.AssertExpectations(t)
|
|
}
|
|
|
|
func TestInitialConfiguration_restored(t *testing.T) {
|
|
dataDir, configDir, builderOpts := testSetupAutoConf(t)
|
|
|
|
cfgFile := filepath.Join(configDir, "test.json")
|
|
require.NoError(t, ioutil.WriteFile(cfgFile, []byte(`{
|
|
"auto_config": {"enabled": true, "intro_token": "blarg", "server_addresses": ["127.0.0.1:8300"]}, "verify_outgoing": true
|
|
}`), 0600))
|
|
|
|
builderOpts.ConfigFiles = append(builderOpts.ConfigFiles, cfgFile)
|
|
|
|
// persist an auto config response to the data dir where it is expected
|
|
persistedFile := filepath.Join(dataDir, autoConfigFileName)
|
|
response := &pbconfig.Config{
|
|
PrimaryDatacenter: "primary",
|
|
}
|
|
data, err := json.Marshal(translateConfig(response))
|
|
require.NoError(t, err)
|
|
require.NoError(t, ioutil.WriteFile(persistedFile, data, 0600))
|
|
|
|
directRPC := mockDirectRPC{}
|
|
|
|
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ac)
|
|
|
|
cfg, err := ac.InitialConfiguration(context.Background())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cfg)
|
|
require.Equal(t, "primary", cfg.PrimaryDatacenter)
|
|
|
|
// ensure no RPC was made
|
|
directRPC.AssertExpectations(t)
|
|
}
|
|
|
|
func TestInitialConfiguration_success(t *testing.T) {
|
|
dataDir, configDir, builderOpts := testSetupAutoConf(t)
|
|
|
|
cfgFile := filepath.Join(configDir, "test.json")
|
|
require.NoError(t, ioutil.WriteFile(cfgFile, []byte(`{
|
|
"auto_config": {"enabled": true, "intro_token": "blarg", "server_addresses": ["127.0.0.1:8300"]}, "verify_outgoing": true
|
|
}`), 0600))
|
|
|
|
builderOpts.ConfigFiles = append(builderOpts.ConfigFiles, cfgFile)
|
|
|
|
persistedFile := filepath.Join(dataDir, autoConfigFileName)
|
|
directRPC := mockDirectRPC{}
|
|
|
|
populateResponse := func(val interface{}) {
|
|
resp, ok := val.(*agentpb.AutoConfigResponse)
|
|
require.True(t, ok)
|
|
resp.Config = &pbconfig.Config{
|
|
PrimaryDatacenter: "primary",
|
|
}
|
|
}
|
|
|
|
expectedRequest := agentpb.AutoConfigRequest{
|
|
Datacenter: "dc1",
|
|
Node: "autoconf",
|
|
JWT: "blarg",
|
|
}
|
|
|
|
directRPC.On(
|
|
"RPC",
|
|
"dc1",
|
|
"autoconf",
|
|
&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8300},
|
|
"AutoConfig.InitialConfiguration",
|
|
&expectedRequest,
|
|
&agentpb.AutoConfigResponse{}).Return(populateResponse)
|
|
|
|
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ac)
|
|
|
|
cfg, err := ac.InitialConfiguration(context.Background())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cfg)
|
|
require.Equal(t, "primary", cfg.PrimaryDatacenter)
|
|
|
|
// the file was written to.
|
|
require.FileExists(t, persistedFile)
|
|
|
|
// ensure no RPC was made
|
|
directRPC.AssertExpectations(t)
|
|
}
|
|
|
|
func TestInitialConfiguration_retries(t *testing.T) {
|
|
dataDir, configDir, builderOpts := testSetupAutoConf(t)
|
|
|
|
cfgFile := filepath.Join(configDir, "test.json")
|
|
require.NoError(t, ioutil.WriteFile(cfgFile, []byte(`{
|
|
"auto_config": {"enabled": true, "intro_token": "blarg", "server_addresses": ["198.18.0.1", "198.18.0.2:8398", "198.18.0.3:8399", "127.0.0.1:1234"]}, "verify_outgoing": true
|
|
}`), 0600))
|
|
|
|
builderOpts.ConfigFiles = append(builderOpts.ConfigFiles, cfgFile)
|
|
|
|
persistedFile := filepath.Join(dataDir, autoConfigFileName)
|
|
directRPC := mockDirectRPC{}
|
|
|
|
populateResponse := func(val interface{}) {
|
|
resp, ok := val.(*agentpb.AutoConfigResponse)
|
|
require.True(t, ok)
|
|
resp.Config = &pbconfig.Config{
|
|
PrimaryDatacenter: "primary",
|
|
}
|
|
}
|
|
|
|
expectedRequest := agentpb.AutoConfigRequest{
|
|
Datacenter: "dc1",
|
|
Node: "autoconf",
|
|
JWT: "blarg",
|
|
}
|
|
|
|
// basically the 198.18.0.* addresses should fail indefinitely. the first time through the
|
|
// outer loop we inject a failure for the DNS resolution of localhost to 127.0.0.1. Then
|
|
// the second time through the outer loop we allow the localhost one to work.
|
|
directRPC.On(
|
|
"RPC",
|
|
"dc1",
|
|
"autoconf",
|
|
&net.TCPAddr{IP: net.IPv4(198, 18, 0, 1), Port: 8300},
|
|
"AutoConfig.InitialConfiguration",
|
|
&expectedRequest,
|
|
&agentpb.AutoConfigResponse{}).Return(fmt.Errorf("injected failure")).Times(0)
|
|
directRPC.On(
|
|
"RPC",
|
|
"dc1",
|
|
"autoconf",
|
|
&net.TCPAddr{IP: net.IPv4(198, 18, 0, 2), Port: 8398},
|
|
"AutoConfig.InitialConfiguration",
|
|
&expectedRequest,
|
|
&agentpb.AutoConfigResponse{}).Return(fmt.Errorf("injected failure")).Times(0)
|
|
directRPC.On(
|
|
"RPC",
|
|
"dc1",
|
|
"autoconf",
|
|
&net.TCPAddr{IP: net.IPv4(198, 18, 0, 3), Port: 8399},
|
|
"AutoConfig.InitialConfiguration",
|
|
&expectedRequest,
|
|
&agentpb.AutoConfigResponse{}).Return(fmt.Errorf("injected failure")).Times(0)
|
|
directRPC.On(
|
|
"RPC",
|
|
"dc1",
|
|
"autoconf",
|
|
&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1234},
|
|
"AutoConfig.InitialConfiguration",
|
|
&expectedRequest,
|
|
&agentpb.AutoConfigResponse{}).Return(fmt.Errorf("injected failure")).Once()
|
|
directRPC.On(
|
|
"RPC",
|
|
"dc1",
|
|
"autoconf",
|
|
&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1234},
|
|
"AutoConfig.InitialConfiguration",
|
|
&expectedRequest,
|
|
&agentpb.AutoConfigResponse{}).Return(populateResponse)
|
|
|
|
waiter := lib.NewRetryWaiter(2, 0, 1*time.Millisecond, nil)
|
|
ac, err := New(WithBuilderOpts(builderOpts), WithTLSConfigurator(&tlsutil.Configurator{}), WithDirectRPC(&directRPC), WithRetryWaiter(waiter))
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ac)
|
|
|
|
cfg, err := ac.InitialConfiguration(context.Background())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, cfg)
|
|
require.Equal(t, "primary", cfg.PrimaryDatacenter)
|
|
|
|
// the file was written to.
|
|
require.FileExists(t, persistedFile)
|
|
|
|
// ensure no RPC was made
|
|
directRPC.AssertExpectations(t)
|
|
}
|