github.com/kjdelisle/consul@v1.4.5/agent/testagent.go (about)

     1  package agent
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"log"
     8  	"math/rand"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"os"
    12  	"path/filepath"
    13  	"strconv"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  
    18  	metrics "github.com/armon/go-metrics"
    19  	uuid "github.com/hashicorp/go-uuid"
    20  
    21  	"github.com/hashicorp/consul/agent/config"
    22  	"github.com/hashicorp/consul/agent/connect"
    23  	"github.com/hashicorp/consul/agent/consul"
    24  	"github.com/hashicorp/consul/agent/structs"
    25  	"github.com/hashicorp/consul/api"
    26  	"github.com/hashicorp/consul/lib/freeport"
    27  	"github.com/hashicorp/consul/logger"
    28  	"github.com/hashicorp/consul/testutil/retry"
    29  
    30  	"github.com/stretchr/testify/require"
    31  )
    32  
    33  func init() {
    34  	rand.Seed(time.Now().UnixNano()) // seed random number generator
    35  }
    36  
    37  // TempDir defines the base dir for temporary directories.
    38  var TempDir = os.TempDir()
    39  
    40  // TestAgent encapsulates an Agent with a default configuration and
    41  // startup procedure suitable for testing. It panics if there are errors
    42  // during creation or startup instead of returning errors. It manages a
    43  // temporary data directory which is removed after shutdown.
    44  type TestAgent struct {
    45  	// Name is an optional name of the agent.
    46  	Name string
    47  
    48  	HCL string
    49  
    50  	// ExpectConfigError can be set to prevent the agent retrying Start on errors
    51  	// and eventually blowing up with runtime.Goexit. This enables tests to assert
    52  	// that some specific bit of config actually does prevent startup entirely in
    53  	// a reasonable way without reproducing a lot of the boilerplate here.
    54  	ExpectConfigError bool
    55  
    56  	// Config is the agent configuration. If Config is nil then
    57  	// TestConfig() is used. If Config.DataDir is set then it is
    58  	// the callers responsibility to clean up the data directory.
    59  	// Otherwise, a temporary data directory is created and removed
    60  	// when Shutdown() is called.
    61  	Config *config.RuntimeConfig
    62  
    63  	// LogOutput is the sink for the logs. If nil, logs are written
    64  	// to os.Stderr.
    65  	LogOutput io.Writer
    66  
    67  	// LogWriter is used for streaming logs.
    68  	LogWriter *logger.LogWriter
    69  
    70  	// DataDir is the data directory which is used when Config.DataDir
    71  	// is not set. It is created automatically and removed when
    72  	// Shutdown() is called.
    73  	DataDir string
    74  
    75  	// Key is the optional encryption key for the LAN and WAN keyring.
    76  	Key string
    77  
    78  	// UseTLS, if true, will disable the HTTP port and enable the HTTPS
    79  	// one.
    80  	UseTLS bool
    81  
    82  	// dns is a reference to the first started DNS endpoint.
    83  	// It is valid after Start().
    84  	dns *DNSServer
    85  
    86  	// srv is a reference to the first started HTTP endpoint.
    87  	// It is valid after Start().
    88  	srv *HTTPServer
    89  
    90  	// Agent is the embedded consul agent.
    91  	// It is valid after Start().
    92  	*Agent
    93  }
    94  
    95  // NewTestAgent returns a started agent with the given name and
    96  // configuration. It fails the test if the Agent could not be started. The
    97  // caller should call Shutdown() to stop the agent and remove temporary
    98  // directories.
    99  func NewTestAgent(t *testing.T, name string, hcl string) *TestAgent {
   100  	a := &TestAgent{Name: name, HCL: hcl}
   101  	a.Start(t)
   102  	return a
   103  }
   104  
   105  func NewUnstartedAgent(t *testing.T, name string, hcl string) (*Agent, error) {
   106  	c := TestConfig(config.Source{Name: name, Format: "hcl", Data: hcl})
   107  	a, err := New(c)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	return a, nil
   112  }
   113  
   114  // Start starts a test agent. It fails the test if the agent could not be started.
   115  func (a *TestAgent) Start(t *testing.T) *TestAgent {
   116  	require := require.New(t)
   117  	require.Nil(a.Agent, "TestAgent already started")
   118  	var hclDataDir string
   119  	if a.DataDir == "" {
   120  		name := "agent"
   121  		if a.Name != "" {
   122  			name = a.Name + "-agent"
   123  		}
   124  		name = strings.Replace(name, "/", "_", -1)
   125  		d, err := ioutil.TempDir(TempDir, name)
   126  		require.NoError(err, fmt.Sprintf("Error creating data dir %s: %s", filepath.Join(TempDir, name), err))
   127  		hclDataDir = `data_dir = "` + d + `"`
   128  	}
   129  	id := NodeID()
   130  
   131  	for i := 10; i >= 0; i-- {
   132  		a.Config = TestConfig(
   133  			randomPortsSource(a.UseTLS),
   134  			config.Source{Name: a.Name, Format: "hcl", Data: a.HCL},
   135  			config.Source{Name: a.Name + ".data_dir", Format: "hcl", Data: hclDataDir},
   136  		)
   137  
   138  		// write the keyring
   139  		if a.Key != "" {
   140  			writeKey := func(key, filename string) {
   141  				path := filepath.Join(a.Config.DataDir, filename)
   142  				err := initKeyring(path, key)
   143  				require.NoError(err, fmt.Sprintf("Error creating keyring %s: %s", path, err))
   144  			}
   145  			writeKey(a.Key, SerfLANKeyring)
   146  			writeKey(a.Key, SerfWANKeyring)
   147  		}
   148  
   149  		agent, err := New(a.Config)
   150  		require.NoError(err, fmt.Sprintf("Error creating agent: %s", err))
   151  
   152  		logOutput := a.LogOutput
   153  		if logOutput == nil {
   154  			logOutput = os.Stderr
   155  		}
   156  		agent.LogOutput = logOutput
   157  		agent.LogWriter = a.LogWriter
   158  		agent.logger = log.New(logOutput, a.Name+" - ", log.LstdFlags|log.Lmicroseconds)
   159  		agent.MemSink = metrics.NewInmemSink(1*time.Second, time.Minute)
   160  
   161  		// we need the err var in the next exit condition
   162  		if err := agent.Start(); err == nil {
   163  			a.Agent = agent
   164  			break
   165  		} else if i == 0 {
   166  			require.Fail("%s %s Error starting agent: %s", id, a.Name, err)
   167  		} else if a.ExpectConfigError {
   168  			// Panic the error since this can be caught if needed. Pretty gross way to
   169  			// detect errors but enough for now and this is a tiny edge case that I'd
   170  			// otherwise not have a way to test at all...
   171  			panic(err)
   172  		} else {
   173  			agent.ShutdownAgent()
   174  			agent.ShutdownEndpoints()
   175  			wait := time.Duration(rand.Int31n(2000)) * time.Millisecond
   176  			fmt.Println(id, a.Name, "retrying in", wait)
   177  			time.Sleep(wait)
   178  		}
   179  
   180  		// Clean out the data dir if we are responsible for it before we
   181  		// try again, since the old ports may have gotten written to
   182  		// the data dir, such as in the Raft configuration.
   183  		if a.DataDir != "" {
   184  			if err := os.RemoveAll(a.DataDir); err != nil {
   185  				require.Fail("%s %s Error resetting data dir: %s", id, a.Name, err)
   186  			}
   187  		}
   188  	}
   189  
   190  	// Start the anti-entropy syncer
   191  	a.Agent.StartSync()
   192  
   193  	var out structs.IndexedNodes
   194  	retry.Run(t, func(r *retry.R) {
   195  		if len(a.httpServers) == 0 {
   196  			r.Fatal(a.Name, "waiting for server")
   197  		}
   198  		if a.Config.Bootstrap && a.Config.ServerMode {
   199  			// Ensure we have a leader and a node registration.
   200  			args := &structs.DCSpecificRequest{
   201  				Datacenter: a.Config.Datacenter,
   202  				QueryOptions: structs.QueryOptions{
   203  					MinQueryIndex: out.Index,
   204  					MaxQueryTime:  25 * time.Millisecond,
   205  				},
   206  			}
   207  			if err := a.RPC("Catalog.ListNodes", args, &out); err != nil {
   208  				r.Fatal(a.Name, "Catalog.ListNodes failed:", err)
   209  			}
   210  			if !out.QueryMeta.KnownLeader {
   211  				r.Fatal(a.Name, "No leader")
   212  			}
   213  			if out.Index == 0 {
   214  				r.Fatal(a.Name, ": Consul index is 0")
   215  			}
   216  		} else {
   217  			req, _ := http.NewRequest("GET", "/v1/agent/self", nil)
   218  			resp := httptest.NewRecorder()
   219  			_, err := a.httpServers[0].AgentSelf(resp, req)
   220  			if err != nil || resp.Code != 200 {
   221  				r.Fatal(a.Name, "failed OK response", err)
   222  			}
   223  		}
   224  	})
   225  	a.dns = a.dnsServers[0]
   226  	a.srv = a.httpServers[0]
   227  	return a
   228  }
   229  
   230  // Shutdown stops the agent and removes the data directory if it is
   231  // managed by the test agent.
   232  func (a *TestAgent) Shutdown() error {
   233  	/* Removed this because it was breaking persistence tests where we would
   234  	persist a service and load it through a new agent with the same data-dir.
   235  	Not sure if we still need this for other things, everywhere we manually make
   236  	a data dir we already do 'defer os.RemoveAll()'
   237  	defer func() {
   238  		if a.DataDir != "" {
   239  			os.RemoveAll(a.DataDir)
   240  		}
   241  	}()*/
   242  
   243  	// shutdown agent before endpoints
   244  	defer a.Agent.ShutdownEndpoints()
   245  	return a.Agent.ShutdownAgent()
   246  }
   247  
   248  func (a *TestAgent) DNSAddr() string {
   249  	if a.dns == nil {
   250  		return ""
   251  	}
   252  	return a.dns.Addr
   253  }
   254  
   255  func (a *TestAgent) HTTPAddr() string {
   256  	if a.srv == nil {
   257  		return ""
   258  	}
   259  	return a.srv.Addr
   260  }
   261  
   262  func (a *TestAgent) SegmentAddr(name string) string {
   263  	if server, ok := a.Agent.delegate.(*consul.Server); ok {
   264  		return server.LANSegmentAddr(name)
   265  	}
   266  	return ""
   267  }
   268  
   269  func (a *TestAgent) Client() *api.Client {
   270  	conf := api.DefaultConfig()
   271  	conf.Address = a.HTTPAddr()
   272  	c, err := api.NewClient(conf)
   273  	if err != nil {
   274  		panic(fmt.Sprintf("Error creating consul API client: %s", err))
   275  	}
   276  	return c
   277  }
   278  
   279  // DNSDisableCompression disables compression for all started DNS servers.
   280  func (a *TestAgent) DNSDisableCompression(b bool) {
   281  	for _, srv := range a.dnsServers {
   282  		srv.disableCompression.Store(b)
   283  	}
   284  }
   285  
   286  func (a *TestAgent) consulConfig() *consul.Config {
   287  	c, err := a.Agent.consulConfig()
   288  	if err != nil {
   289  		panic(err)
   290  	}
   291  	return c
   292  }
   293  
   294  // pickRandomPorts selects random ports from fixed size random blocks of
   295  // ports. This does not eliminate the chance for port conflict but
   296  // reduces it significantly with little overhead. Furthermore, asking
   297  // the kernel for a random port by binding to port 0 prolongs the test
   298  // execution (in our case +20sec) while also not fully eliminating the
   299  // chance of port conflicts for concurrently executed test binaries.
   300  // Instead of relying on one set of ports to be sufficient we retry
   301  // starting the agent with different ports on port conflict.
   302  func randomPortsSource(tls bool) config.Source {
   303  	ports := freeport.Get(6)
   304  	if tls {
   305  		ports[1] = -1
   306  	} else {
   307  		ports[2] = -1
   308  	}
   309  	return config.Source{
   310  		Name:   "ports",
   311  		Format: "hcl",
   312  		Data: `
   313  			ports = {
   314  				dns = ` + strconv.Itoa(ports[0]) + `
   315  				http = ` + strconv.Itoa(ports[1]) + `
   316  				https = ` + strconv.Itoa(ports[2]) + `
   317  				serf_lan = ` + strconv.Itoa(ports[3]) + `
   318  				serf_wan = ` + strconv.Itoa(ports[4]) + `
   319  				server = ` + strconv.Itoa(ports[5]) + `
   320  			}
   321  		`,
   322  	}
   323  }
   324  
   325  func NodeID() string {
   326  	id, err := uuid.GenerateUUID()
   327  	if err != nil {
   328  		panic(err)
   329  	}
   330  	return id
   331  }
   332  
   333  // TestConfig returns a unique default configuration for testing an
   334  // agent.
   335  func TestConfig(sources ...config.Source) *config.RuntimeConfig {
   336  	nodeID := NodeID()
   337  	testsrc := config.Source{
   338  		Name:   "test",
   339  		Format: "hcl",
   340  		Data: `
   341  			bind_addr = "127.0.0.1"
   342  			advertise_addr = "127.0.0.1"
   343  			datacenter = "dc1"
   344  			bootstrap = true
   345  			server = true
   346  			node_id = "` + nodeID + `"
   347  			node_name = "Node ` + nodeID + `"
   348  			connect {
   349  				enabled = true
   350  				ca_config {
   351  					cluster_id = "` + connect.TestClusterID + `"
   352  				}
   353  			}
   354  			performance {
   355  				raft_multiplier = 1
   356  			}
   357  		`,
   358  	}
   359  
   360  	b, err := config.NewBuilder(config.Flags{})
   361  	if err != nil {
   362  		panic("NewBuilder failed: " + err.Error())
   363  	}
   364  	b.Head = append(b.Head, testsrc)
   365  	b.Tail = append(b.Tail, config.DefaultConsulSource(), config.DevConsulSource())
   366  	b.Tail = append(b.Tail, sources...)
   367  
   368  	cfg, err := b.BuildAndValidate()
   369  	if err != nil {
   370  		panic("Error building config: " + err.Error())
   371  	}
   372  
   373  	for _, w := range b.Warnings {
   374  		fmt.Println("WARNING:", w)
   375  	}
   376  
   377  	// Disable connect proxy execution since it causes all kinds of problems with
   378  	// self-executing tests etc.
   379  	cfg.ConnectTestDisableManagedProxies = true
   380  	// Effectively disables the delay after root rotation before requesting CSRs
   381  	// to make test deterministic. 0 results in default jitter being applied but a
   382  	// tiny delay is effectively thre same.
   383  	cfg.ConnectTestCALeafRootChangeSpread = 1 * time.Nanosecond
   384  
   385  	return &cfg
   386  }
   387  
   388  // TestACLConfig returns a default configuration for testing an agent
   389  // with ACLs.
   390  func TestACLConfig() string {
   391  	return `
   392  		acl_datacenter = "dc1"
   393  		acl_default_policy = "deny"
   394  		acl_master_token = "root"
   395  		acl_agent_token = "root"
   396  		acl_agent_master_token = "towel"
   397  		acl_enforce_version_8 = true
   398  	`
   399  }