github.com/outbrain/consul@v1.4.5/agent/proxyprocess/manager_test.go (about)

     1  package proxyprocess
     2  
     3  import (
     4  	"io/ioutil"
     5  	"os"
     6  	"os/exec"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/hashicorp/consul/agent/local"
    14  	"github.com/hashicorp/consul/agent/structs"
    15  	"github.com/hashicorp/consul/testutil/retry"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  func TestManagerClose_noRun(t *testing.T) {
    20  	t.Parallel()
    21  
    22  	// Really we're testing that it doesn't deadlock here.
    23  	m, closer := testManager(t)
    24  	defer closer()
    25  	require.NoError(t, m.Close())
    26  
    27  	// Close again for sanity
    28  	require.NoError(t, m.Close())
    29  }
    30  
    31  // Test that Run performs an initial sync (if local.State is already set)
    32  // rather than waiting for a notification from the local state.
    33  func TestManagerRun_initialSync(t *testing.T) {
    34  	t.Parallel()
    35  
    36  	state := local.TestState(t)
    37  	m, closer := testManager(t)
    38  	defer closer()
    39  	m.State = state
    40  	defer m.Kill()
    41  
    42  	// Add the proxy before we start the manager to verify initial sync
    43  	td, closer := testTempDir(t)
    44  	defer closer()
    45  	path := filepath.Join(td, "file")
    46  
    47  	cmd, destroy := helperProcess("restart", path)
    48  	defer destroy()
    49  
    50  	testStateProxy(t, state, "web", cmd)
    51  
    52  	// Start the manager
    53  	go m.Run()
    54  
    55  	// We should see the path appear shortly
    56  	retry.Run(t, func(r *retry.R) {
    57  		_, err := os.Stat(path)
    58  		if err == nil {
    59  			return
    60  		}
    61  		r.Fatalf("error waiting for path: %s", err)
    62  	})
    63  }
    64  
    65  func TestManagerRun_syncNew(t *testing.T) {
    66  	t.Parallel()
    67  
    68  	state := local.TestState(t)
    69  	m, closer := testManager(t)
    70  	defer closer()
    71  	m.State = state
    72  	defer m.Kill()
    73  
    74  	// Start the manager
    75  	go m.Run()
    76  
    77  	// Sleep a bit, this is just an attempt for Run to already be running.
    78  	// Its not a big deal if this sleep doesn't happen (slow CI).
    79  	time.Sleep(100 * time.Millisecond)
    80  
    81  	// Add the first proxy
    82  	td, closer := testTempDir(t)
    83  	defer closer()
    84  	path := filepath.Join(td, "file")
    85  
    86  	cmd, destroy := helperProcess("restart", path)
    87  	defer destroy()
    88  
    89  	testStateProxy(t, state, "web", cmd)
    90  
    91  	// We should see the path appear shortly
    92  	retry.Run(t, func(r *retry.R) {
    93  		_, err := os.Stat(path)
    94  		if err == nil {
    95  			return
    96  		}
    97  		r.Fatalf("error waiting for path: %s", err)
    98  	})
    99  
   100  	// Add another proxy
   101  	path = path + "2"
   102  
   103  	cmd, destroy = helperProcess("restart", path)
   104  	defer destroy()
   105  
   106  	testStateProxy(t, state, "db", cmd)
   107  	retry.Run(t, func(r *retry.R) {
   108  		_, err := os.Stat(path)
   109  		if err == nil {
   110  			return
   111  		}
   112  		r.Fatalf("error waiting for path: %s", err)
   113  	})
   114  }
   115  
   116  func TestManagerRun_syncDelete(t *testing.T) {
   117  	t.Parallel()
   118  
   119  	state := local.TestState(t)
   120  	m, closer := testManager(t)
   121  	defer closer()
   122  	m.State = state
   123  	defer m.Kill()
   124  
   125  	// Start the manager
   126  	go m.Run()
   127  
   128  	// Add the first proxy
   129  	td, closer := testTempDir(t)
   130  	defer closer()
   131  	path := filepath.Join(td, "file")
   132  
   133  	cmd, destroy := helperProcess("restart", path)
   134  	defer destroy()
   135  
   136  	id := testStateProxy(t, state, "web", cmd)
   137  
   138  	// We should see the path appear shortly
   139  	retry.Run(t, func(r *retry.R) {
   140  		_, err := os.Stat(path)
   141  		if err == nil {
   142  			return
   143  		}
   144  		r.Fatalf("error waiting for path: %s", err)
   145  	})
   146  
   147  	// Remove the proxy
   148  	_, err := state.RemoveProxy(id)
   149  	require.NoError(t, err)
   150  
   151  	// File should disappear as process is killed
   152  	retry.Run(t, func(r *retry.R) {
   153  		_, err := os.Stat(path)
   154  		if err == nil {
   155  			r.Fatalf("path exists")
   156  		}
   157  	})
   158  }
   159  
   160  func TestManagerRun_syncUpdate(t *testing.T) {
   161  	t.Parallel()
   162  
   163  	state := local.TestState(t)
   164  	m, closer := testManager(t)
   165  	defer closer()
   166  	m.State = state
   167  	defer m.Kill()
   168  
   169  	// Start the manager
   170  	go m.Run()
   171  
   172  	// Add the first proxy
   173  	td, closer := testTempDir(t)
   174  	defer closer()
   175  	path := filepath.Join(td, "file")
   176  
   177  	cmd, destroy := helperProcess("restart", path)
   178  	defer destroy()
   179  
   180  	testStateProxy(t, state, "web", cmd)
   181  
   182  	// We should see the path appear shortly
   183  	retry.Run(t, func(r *retry.R) {
   184  		_, err := os.Stat(path)
   185  		if err == nil {
   186  			return
   187  		}
   188  		r.Fatalf("error waiting for path: %s", err)
   189  	})
   190  
   191  	// Update the proxy with a new path
   192  	oldPath := path
   193  	path = path + "2"
   194  
   195  	cmd, destroy = helperProcess("restart", path)
   196  	defer destroy()
   197  
   198  	testStateProxy(t, state, "web", cmd)
   199  
   200  	retry.Run(t, func(r *retry.R) {
   201  		_, err := os.Stat(path)
   202  		if err == nil {
   203  			return
   204  		}
   205  		r.Fatalf("error waiting for path: %s", err)
   206  	})
   207  
   208  	// Old path should be gone
   209  	retry.Run(t, func(r *retry.R) {
   210  		_, err := os.Stat(oldPath)
   211  		if err == nil {
   212  			r.Fatalf("old path exists")
   213  		}
   214  	})
   215  }
   216  
   217  func TestManagerRun_daemonLogs(t *testing.T) {
   218  	t.Parallel()
   219  
   220  	require := require.New(t)
   221  	state := local.TestState(t)
   222  	m, closer := testManager(t)
   223  	defer closer()
   224  	m.State = state
   225  	defer m.Kill()
   226  
   227  	// Configure a log dir so that we can read the logs
   228  	logDir := filepath.Join(m.DataDir, "logs")
   229  
   230  	// Create the service and calculate the log paths
   231  	path := filepath.Join(m.DataDir, "notify")
   232  
   233  	cmd, destroy := helperProcess("output", path)
   234  	defer destroy()
   235  
   236  	id := testStateProxy(t, state, "web", cmd)
   237  	stdoutPath := logPath(logDir, id, "stdout")
   238  	stderrPath := logPath(logDir, id, "stderr")
   239  
   240  	// Start the manager
   241  	go m.Run()
   242  
   243  	// We should see the path appear shortly
   244  	retry.Run(t, func(r *retry.R) {
   245  		if _, err := os.Stat(path); err != nil {
   246  			r.Fatalf("error waiting for stdout path: %s", err)
   247  		}
   248  	})
   249  
   250  	expectedOut := "hello stdout\n"
   251  	actual, err := ioutil.ReadFile(stdoutPath)
   252  	require.NoError(err)
   253  	require.Equal([]byte(expectedOut), actual)
   254  
   255  	expectedErr := "hello stderr\n"
   256  	actual, err = ioutil.ReadFile(stderrPath)
   257  	require.NoError(err)
   258  	require.Equal([]byte(expectedErr), actual)
   259  }
   260  
   261  func TestManagerRun_daemonPid(t *testing.T) {
   262  	t.Parallel()
   263  
   264  	require := require.New(t)
   265  	state := local.TestState(t)
   266  	m, closer := testManager(t)
   267  	defer closer()
   268  	m.State = state
   269  	defer m.Kill()
   270  
   271  	// Configure a log dir so that we can read the logs
   272  	pidDir := filepath.Join(m.DataDir, "pids")
   273  
   274  	// Create the service and calculate the log paths
   275  	path := filepath.Join(m.DataDir, "notify")
   276  
   277  	cmd, destroy := helperProcess("output", path)
   278  	defer destroy()
   279  
   280  	id := testStateProxy(t, state, "web", cmd)
   281  	pidPath := pidPath(pidDir, id)
   282  
   283  	// Start the manager
   284  	go m.Run()
   285  
   286  	// We should see the path appear shortly
   287  	retry.Run(t, func(r *retry.R) {
   288  		if _, err := os.Stat(path); err != nil {
   289  			r.Fatalf("error waiting for stdout path: %s", err)
   290  		}
   291  	})
   292  
   293  	// Verify the pid file is not empty
   294  	pidRaw, err := ioutil.ReadFile(pidPath)
   295  	require.NoError(err)
   296  	require.NotEmpty(pidRaw)
   297  }
   298  
   299  // Test to check if the parent and the child processes
   300  // have the same environmental variables
   301  
   302  func TestManagerPassesEnvironment(t *testing.T) {
   303  	t.Parallel()
   304  
   305  	require := require.New(t)
   306  	state := local.TestState(t)
   307  	m, closer := testManager(t)
   308  	defer closer()
   309  	m.State = state
   310  	defer m.Kill()
   311  
   312  	// Add Proxy for the test
   313  	td, closer := testTempDir(t)
   314  	defer closer()
   315  	path := filepath.Join(td, "env-variables")
   316  
   317  	cmd, destroy := helperProcess("environ", path)
   318  	defer destroy()
   319  
   320  	testStateProxy(t, state, "environTest", cmd)
   321  
   322  	//Run the manager
   323  	go m.Run()
   324  
   325  	//Get the environmental variables from the OS
   326  	var fileContent []byte
   327  	var err error
   328  	var data []byte
   329  	envData := os.Environ()
   330  	sort.Strings(envData)
   331  	for _, envVariable := range envData {
   332  		if strings.HasPrefix(envVariable, "CONSUL") || strings.HasPrefix(envVariable, "CONNECT") {
   333  			continue
   334  		}
   335  		data = append(data, envVariable...)
   336  		data = append(data, "\n"...)
   337  	}
   338  
   339  	// Check if the file written to from the spawned process
   340  	// has the necessary environmental variable data
   341  	retry.Run(t, func(r *retry.R) {
   342  		if fileContent, err = ioutil.ReadFile(path); err != nil {
   343  			r.Fatalf("No file ya dummy")
   344  		}
   345  	})
   346  
   347  	require.Equal(data, fileContent)
   348  }
   349  
   350  // Test to check if the parent and the child processes
   351  // have the same environmental variables
   352  func TestManagerPassesProxyEnv(t *testing.T) {
   353  	t.Parallel()
   354  
   355  	require := require.New(t)
   356  	state := local.TestState(t)
   357  	m, closer := testManager(t)
   358  	defer closer()
   359  	m.State = state
   360  	defer m.Kill()
   361  
   362  	penv := make([]string, 0, 2)
   363  	penv = append(penv, "HTTP_ADDR=127.0.0.1:8500")
   364  	penv = append(penv, "HTTP_SSL=false")
   365  	m.ProxyEnv = penv
   366  
   367  	// Add Proxy for the test
   368  	td, closer := testTempDir(t)
   369  	defer closer()
   370  	path := filepath.Join(td, "env-variables")
   371  
   372  	cmd, destroy := helperProcess("environ", path)
   373  	defer destroy()
   374  
   375  	testStateProxy(t, state, "environTest", cmd)
   376  
   377  	//Run the manager
   378  	go m.Run()
   379  
   380  	//Get the environmental variables from the OS
   381  	var fileContent []byte
   382  	var err error
   383  	var data []byte
   384  	envData := os.Environ()
   385  	envData = append(envData, "HTTP_ADDR=127.0.0.1:8500")
   386  	envData = append(envData, "HTTP_SSL=false")
   387  	sort.Strings(envData)
   388  	for _, envVariable := range envData {
   389  		if strings.HasPrefix(envVariable, "CONSUL") || strings.HasPrefix(envVariable, "CONNECT") {
   390  			continue
   391  		}
   392  		data = append(data, envVariable...)
   393  		data = append(data, "\n"...)
   394  	}
   395  
   396  	// Check if the file written to from the spawned process
   397  	// has the necessary environmental variable data
   398  	retry.Run(t, func(r *retry.R) {
   399  		if fileContent, err = ioutil.ReadFile(path); err != nil {
   400  			r.Fatalf("No file ya dummy")
   401  		}
   402  	})
   403  
   404  	require.Equal(data, fileContent)
   405  }
   406  
   407  // Test the Snapshot/Restore works.
   408  func TestManagerRun_snapshotRestore(t *testing.T) {
   409  	t.Parallel()
   410  
   411  	require := require.New(t)
   412  	state := local.TestState(t)
   413  	m, closer := testManager(t)
   414  	defer closer()
   415  	m.State = state
   416  	defer m.Kill()
   417  
   418  	// Add the proxy
   419  	td, closer := testTempDir(t)
   420  	defer closer()
   421  	path := filepath.Join(td, "file")
   422  
   423  	cmd, destroy := helperProcess("start-stop", path)
   424  	defer destroy()
   425  
   426  	testStateProxy(t, state, "web", cmd)
   427  
   428  	// Set a low snapshot period so we get a snapshot
   429  	m.SnapshotPeriod = 10 * time.Millisecond
   430  
   431  	// Start the manager
   432  	go m.Run()
   433  
   434  	// We should see the path appear shortly
   435  	retry.Run(t, func(r *retry.R) {
   436  		_, err := os.Stat(path)
   437  		if err == nil {
   438  			return
   439  		}
   440  		r.Fatalf("error waiting for path: %s", err)
   441  	})
   442  
   443  	// Wait for the snapshot
   444  	snapPath := m.SnapshotPath()
   445  	retry.Run(t, func(r *retry.R) {
   446  		raw, err := ioutil.ReadFile(snapPath)
   447  		if err != nil {
   448  			r.Fatalf("error waiting for path: %s", err)
   449  		}
   450  		if len(raw) < 30 {
   451  			r.Fatalf("snapshot too small")
   452  		}
   453  	})
   454  
   455  	// Stop the sync
   456  	require.NoError(m.Close())
   457  
   458  	// File should still exist
   459  	_, err := os.Stat(path)
   460  	require.NoError(err)
   461  
   462  	// Restore a manager from a snapshot
   463  	m2, closer := testManager(t)
   464  	m2.State = state
   465  	defer closer()
   466  	defer m2.Kill()
   467  	require.NoError(m2.Restore(snapPath))
   468  
   469  	// Start
   470  	go m2.Run()
   471  
   472  	// Add a second proxy so that we can determine when we're up
   473  	// and running.
   474  	path2 := filepath.Join(td, "file2")
   475  
   476  	cmd, destroy = helperProcess("start-stop", path2)
   477  	defer destroy()
   478  
   479  	testStateProxy(t, state, "db", cmd)
   480  
   481  	retry.Run(t, func(r *retry.R) {
   482  		_, err := os.Stat(path2)
   483  		if err == nil {
   484  			return
   485  		}
   486  		r.Fatalf("error waiting for path: %s", err)
   487  	})
   488  
   489  	// Kill m2, which should kill our main process
   490  	require.NoError(m2.Kill())
   491  
   492  	// File should no longer exist
   493  	retry.Run(t, func(r *retry.R) {
   494  		_, err := os.Stat(path)
   495  		if err != nil {
   496  			return
   497  		}
   498  		r.Fatalf("file still exists: %s", path)
   499  	})
   500  }
   501  
   502  // Manager should not run any proxies if we're running as root. Tests
   503  // stub the value.
   504  func TestManagerRun_rootDisallow(t *testing.T) {
   505  	// Pretend we are root
   506  	defer testSetRootValue(true)()
   507  
   508  	state := local.TestState(t)
   509  	m, closer := testManager(t)
   510  	defer closer()
   511  	m.State = state
   512  	defer m.Kill()
   513  
   514  	// Add the proxy before we start the manager to verify initial sync
   515  	td, closer := testTempDir(t)
   516  	defer closer()
   517  	path := filepath.Join(td, "file")
   518  
   519  	cmd, destroy := helperProcess("restart", path)
   520  	defer destroy()
   521  
   522  	testStateProxy(t, state, "web", cmd)
   523  
   524  	// Start the manager
   525  	go m.Run()
   526  
   527  	// Sleep a bit just to verify
   528  	time.Sleep(100 * time.Millisecond)
   529  
   530  	// We should see the path appear shortly
   531  	retry.Run(t, func(r *retry.R) {
   532  		_, err := os.Stat(path)
   533  		if err != nil {
   534  			return
   535  		}
   536  
   537  		r.Fatalf("path exists")
   538  	})
   539  }
   540  
   541  func testManager(t *testing.T) (*Manager, func()) {
   542  	m := NewManager()
   543  
   544  	// Setup a default state
   545  	m.State = local.TestState(t)
   546  
   547  	// Set these periods low to speed up tests
   548  	m.CoalescePeriod = 1 * time.Millisecond
   549  	m.QuiescentPeriod = 1 * time.Millisecond
   550  
   551  	// Setup a temporary directory for logs
   552  	td, closer := testTempDir(t)
   553  	m.DataDir = td
   554  
   555  	return m, func() { closer() }
   556  }
   557  
   558  // testStateProxy registers a proxy with the given local state and the command
   559  // (expected to be from the helperProcess function call). It returns the
   560  // ID for deregistration.
   561  func testStateProxy(t *testing.T, state *local.State, service string, cmd *exec.Cmd) string {
   562  	// *exec.Cmd must manually set args[0] to the binary. We automatically
   563  	// set this when constructing the command for the proxy, so we must strip
   564  	// the zero index. We do this unconditionally (anytime len is > 0) because
   565  	// index zero should ALWAYS be the binary.
   566  	if len(cmd.Args) > 0 {
   567  		cmd.Args = cmd.Args[1:]
   568  	}
   569  
   570  	command := []string{cmd.Path}
   571  	command = append(command, cmd.Args...)
   572  
   573  	require.NoError(t, state.AddService(&structs.NodeService{
   574  		Service: service,
   575  	}, "token"))
   576  
   577  	p, err := state.AddProxy(&structs.ConnectManagedProxy{
   578  		ExecMode:        structs.ProxyExecModeDaemon,
   579  		Command:         command,
   580  		TargetServiceID: service,
   581  	}, "token", "")
   582  	require.NoError(t, err)
   583  
   584  	return p.Proxy.ProxyService.ID
   585  }