consul/agent/proxy/proxy_test.go

231 lines
6.1 KiB
Go

package proxy
import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"os/signal"
"strconv"
"testing"
"time"
"github.com/hashicorp/consul/command/connect/daemonize"
"github.com/mitchellh/cli"
)
// 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
}
// helperProcessDaemon returns a *Daemon that can be used to execute the
// TestHelperProcess function below. This can be used to test multi-process
// interactions. The Daemon has it's Path, Args and stdio paths populated but
// other fields might need to be set depending on test requirements.
//
// This relies on the TestMainInterceptDaemonize hack being active.
func helperProcessDaemon(s ...string) *Daemon {
cs := []string{os.Args[0], "-test.run=TestHelperProcess", "--", helperProcessSentinel}
cs = append(cs, s...)
return &Daemon{
Path: os.Args[0],
Args: cs,
StdoutPath: "_", // dev null them for now
StderrPath: "_",
daemonizeCmd: helperProcess("daemonize").Args,
}
}
// 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)
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)
}
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 exits. 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 caller 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.
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 := helperProcess(args[1:]...)
if err := d.Start(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
// Write PID
pidBs := []byte(strconv.Itoa(d.Process.Pid))
if err := ioutil.WriteFile(pidFile, pidBs, 0644); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
os.Exit(1)
}
// Wait "forever" (calling test chooses when we exit with signal/Wait to
// minimise coordination)
for {
time.Sleep(time.Hour)
}
case "daemonize":
// Run daemonize!
ui := &cli.BasicUi{Writer: os.Stdout, ErrorWriter: os.Stderr}
cli := &cli.CLI{
Args: append([]string{"daemonize"}, args...),
Commands: map[string]cli.CommandFactory{
"daemonize": func() (cli.Command, error) {
return daemonize.New(ui), nil
},
},
}
exitCode, err := cli.Run()
if err != nil {
log.Printf("[ERR] running hijacked daemonize command: %s", err)
}
os.Exit(exitCode)
default:
fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd)
os.Exit(2)
}
}