open-consul/agent/proxyprocess/proxy_test.go
Paul Banks bed72f6078 Rename proxy package (re-run of #4550) (#4638)
* Rename agent/proxy package to reflect that it is limited to managed proxy processes

Rationale: we have several other components of the agent that relate to Connect proxies for example the ProxyConfigManager component needed for Envoy work. Those things are pretty separate from the focus of this package so far which is only concerned with managing external proxy processes so it's nota good fit to put code for that in here, yet there is a naming clash if we have other packages related to proxy functionality that are not in the `agent/proxy` package.

Happy to bikeshed the name. I started by calling it `managedproxy` but `managedproxy.Manager` is especially unpleasant. `proxyprocess` seems good in that it's more specific about purpose but less clearly connected with the concept of "managed proxies". The names in use are cleaner though e.g. `proxyprocess.Manager`.

This rename was completed automatically using golang.org/x/tools/cmd/gomvpkg.

Depends on #4541

* Fix missed windows tagged files
2018-10-10 16:55:34 +01:00

254 lines
6.4 KiB
Go

package proxyprocess
import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"os/signal"
"sort"
"strconv"
"strings"
"syscall"
"testing"
"time"
)
// testLogger is a logger that can be used by tests that require a
// *log.Logger instance.
var testLogger = log.New(os.Stderr, "logger: ", log.LstdFlags)
// testTempDir returns a temporary directory and a cleanup function.
func testTempDir(t *testing.T) (string, func()) {
t.Helper()
td, err := ioutil.TempDir("", "test-agent-proxy")
if err != nil {
t.Fatalf("err: %s", err)
}
return td, func() {
if err := os.RemoveAll(td); err != nil {
t.Fatalf("err: %s", err)
}
}
}
// helperProcessSentinel is a sentinel value that is put as the first
// argument following "--" and is used to determine if TestHelperProcess
// should run.
const helperProcessSentinel = "WANT_HELPER_PROCESS"
// helperProcess returns an *exec.Cmd that can be used to execute the
// TestHelperProcess function below. This can be used to test multi-process
// interactions.
func helperProcess(s ...string) *exec.Cmd {
cs := []string{"-test.run=TestHelperProcess", "--", helperProcessSentinel}
cs = append(cs, s...)
cmd := exec.Command(os.Args[0], cs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd
}
// This is not a real test. This is just a helper process kicked off by tests
// using the helperProcess helper function.
func TestHelperProcess(t *testing.T) {
args := os.Args
for len(args) > 0 {
if args[0] == "--" {
args = args[1:]
break
}
args = args[1:]
}
if len(args) == 0 || args[0] != helperProcessSentinel {
return
}
defer os.Exit(0)
args = args[1:] // strip sentinel value
cmd, args := args[0], args[1:]
switch cmd {
// While running, this creates a file in the given directory (args[0])
// and deletes it only when it is stopped.
case "start-stop":
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(ch)
path := args[0]
var data []byte
data = append(data, []byte(os.Getenv(EnvProxyID))...)
data = append(data, ':')
data = append(data, []byte(os.Getenv(EnvProxyToken))...)
if err := ioutil.WriteFile(path, data, 0644); err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(path)
<-ch
// Restart writes to a file and keeps running while that file still
// exists. When that file is removed, this process exits. This can be
// used to test restarting.
case "restart":
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
defer signal.Stop(ch)
// Write the file
path := args[0]
if err := ioutil.WriteFile(path, []byte("hello"), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
// While the file still exists, do nothing. When the file no longer
// exists, we exit.
for {
time.Sleep(25 * time.Millisecond)
if _, err := os.Stat(path); os.IsNotExist(err) {
break
}
select {
case <-ch:
// We received an interrupt, clean exit
os.Remove(path)
break
default:
}
}
case "stop-kill":
// Setup listeners so it is ignored
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
defer signal.Stop(ch)
path := args[0]
data := []byte(os.Getenv(EnvProxyToken))
for {
if err := ioutil.WriteFile(path, data, 0644); err != nil {
t.Fatalf("err: %s", err)
}
time.Sleep(25 * time.Millisecond)
}
// Check if the external process can access the enivironmental variables
case "environ":
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
defer signal.Stop(stop)
//Get the path for the file to be written to
path := args[0]
var data []byte
//Get the environmental variables
envData := os.Environ()
//Sort the env data for easier comparison
sort.Strings(envData)
for _, envVariable := range envData {
if strings.HasPrefix(envVariable, "CONSUL") || strings.HasPrefix(envVariable, "CONNECT") {
continue
}
data = append(data, envVariable...)
data = append(data, "\n"...)
}
if err := ioutil.WriteFile(path, data, 0644); err != nil {
t.Fatalf("[Error] File write failed : %s", err)
}
// Clean up after we receive the signal to exit
defer os.Remove(path)
<-stop
case "output":
fmt.Fprintf(os.Stdout, "hello stdout\n")
fmt.Fprintf(os.Stderr, "hello stderr\n")
// Sync to be sure it is written out of buffers
os.Stdout.Sync()
os.Stderr.Sync()
// Output a file to signal we've written to stdout/err
path := args[0]
if err := ioutil.WriteFile(path, []byte("hello"), 0644); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
<-make(chan struct{})
// Parent runs the given process in a Daemon and then sleeps until the test
// code kills it. It exists to test that the Daemon-managed child process
// survives it's parent exiting which we can't test directly without exiting
// the test process so we need an extra level of indirection. The test code
// using this must pass a file path as the first argument for the child
// processes PID to be written and then must take care to clean up that PID
// later or the child will be left running forever.
//
// If the PID file already exists, it will "adopt" the child rather than
// launch a new one.
case "parent":
// We will write the PID for the child to the file in the first argument
// then pass rest of args through to command.
pidFile := args[0]
d := &Daemon{
Command: helperProcess(args[1:]...),
Logger: testLogger,
PidPath: pidFile,
}
_, err := os.Stat(pidFile)
if err == nil {
// pidFile exists, read it and "adopt" the process
bs, err := ioutil.ReadFile(pidFile)
if err != nil {
log.Printf("Error: %s", err)
os.Exit(1)
}
pid, err := strconv.Atoi(string(bs))
if err != nil {
log.Printf("Error: %s", err)
os.Exit(1)
}
// Make a fake snapshot to load
snapshot := map[string]interface{}{
"Pid": pid,
"CommandPath": d.Command.Path,
"CommandArgs": d.Command.Args,
"CommandDir": d.Command.Dir,
"CommandEnv": d.Command.Env,
"ProxyToken": "",
}
d.UnmarshalSnapshot(snapshot)
}
if err := d.Start(); err != nil {
log.Printf("Error: %s", err)
os.Exit(1)
}
log.Println("Started child")
// Wait "forever" (calling test chooses when we exit with signal/Wait to
// minimise coordination).
for {
time.Sleep(time.Hour)
}
default:
fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd)
os.Exit(2)
}
}