Make daemoinze an option on test binary without hacks. Misc fixes for racey or broken tests. Still failing on several though.

This commit is contained in:
Paul Banks 2018-05-23 17:54:50 +01:00 committed by Jack Pearkes
parent 2b377dc624
commit ba0fb58a72
8 changed files with 146 additions and 51 deletions

View File

@ -1691,7 +1691,7 @@ func TestStateProxyManagement(t *testing.T) {
require := require.New(t) require := require.New(t)
assert := assert.New(t) assert := assert.New(t)
_, err := state.AddProxy(&p1, "fake-token") _, err := state.AddProxy(&p1, "fake-token", "")
require.Error(err, "should fail as the target service isn't registered") require.Error(err, "should fail as the target service isn't registered")
// Sanity check done, lets add a couple of target services to the state // Sanity check done, lets add a couple of target services to the state
@ -1710,7 +1710,7 @@ func TestStateProxyManagement(t *testing.T) {
require.NoError(err) require.NoError(err)
// Should work now // Should work now
pstate, err := state.AddProxy(&p1, "fake-token") pstate, err := state.AddProxy(&p1, "fake-token", "")
require.NoError(err) require.NoError(err)
svc := pstate.Proxy.ProxyService svc := pstate.Proxy.ProxyService
@ -1724,8 +1724,9 @@ func TestStateProxyManagement(t *testing.T) {
{ {
// Re-registering same proxy again should not pick a random port but re-use // Re-registering same proxy again should not pick a random port but re-use
// the assigned one. // the assigned one. It should also keep the same proxy token since we don't
pstateDup, err := state.AddProxy(&p1, "fake-token") // want to force restart for config change.
pstateDup, err := state.AddProxy(&p1, "fake-token", "")
require.NoError(err) require.NoError(err)
svcDup := pstateDup.Proxy.ProxyService svcDup := pstateDup.Proxy.ProxyService
@ -1736,6 +1737,8 @@ func TestStateProxyManagement(t *testing.T) {
assert.Equal("", svcDup.Address, "should have empty address by default") assert.Equal("", svcDup.Address, "should have empty address by default")
// Port must be same as before // Port must be same as before
assert.Equal(svc.Port, svcDup.Port) assert.Equal(svc.Port, svcDup.Port)
// Same ProxyToken
assert.Equal(pstate.ProxyToken, pstateDup.ProxyToken)
} }
// Let's register a notifier now // Let's register a notifier now
@ -1748,7 +1751,7 @@ func TestStateProxyManagement(t *testing.T) {
// Second proxy should claim other port // Second proxy should claim other port
p2 := p1 p2 := p1
p2.TargetServiceID = "cache" p2.TargetServiceID = "cache"
pstate2, err := state.AddProxy(&p2, "fake-token") pstate2, err := state.AddProxy(&p2, "fake-token", "")
require.NoError(err) require.NoError(err)
svc2 := pstate2.Proxy.ProxyService svc2 := pstate2.Proxy.ProxyService
assert.Contains([]int{20000, 20001}, svc2.Port) assert.Contains([]int{20000, 20001}, svc2.Port)
@ -1764,7 +1767,7 @@ func TestStateProxyManagement(t *testing.T) {
// Third proxy should fail as all ports are used // Third proxy should fail as all ports are used
p3 := p1 p3 := p1
p3.TargetServiceID = "db" p3.TargetServiceID = "db"
_, err = state.AddProxy(&p3, "fake-token") _, err = state.AddProxy(&p3, "fake-token", "")
require.Error(err) require.Error(err)
// Should have a notification but we'll do nothing so that the next // Should have a notification but we'll do nothing so that the next
@ -1775,7 +1778,7 @@ func TestStateProxyManagement(t *testing.T) {
"bind_port": 1234, "bind_port": 1234,
"bind_address": "0.0.0.0", "bind_address": "0.0.0.0",
} }
pstate3, err := state.AddProxy(&p3, "fake-token") pstate3, err := state.AddProxy(&p3, "fake-token", "")
require.NoError(err) require.NoError(err)
svc3 := pstate3.Proxy.ProxyService svc3 := pstate3.Proxy.ProxyService
require.Equal("0.0.0.0", svc3.Address) require.Equal("0.0.0.0", svc3.Address)
@ -1793,7 +1796,7 @@ func TestStateProxyManagement(t *testing.T) {
require.NotNil(gotP3) require.NotNil(gotP3)
var ws memdb.WatchSet var ws memdb.WatchSet
ws.Add(gotP3.WatchCh) ws.Add(gotP3.WatchCh)
pstate3, err = state.AddProxy(&p3updated, "fake-token") pstate3, err = state.AddProxy(&p3updated, "fake-token", "")
require.NoError(err) require.NoError(err)
svc3 = pstate3.Proxy.ProxyService svc3 = pstate3.Proxy.ProxyService
require.Equal("0.0.0.0", svc3.Address) require.Equal("0.0.0.0", svc3.Address)
@ -1817,7 +1820,7 @@ func TestStateProxyManagement(t *testing.T) {
// Should be able to create a new proxy for that service with the port (it // Should be able to create a new proxy for that service with the port (it
// should have been "freed"). // should have been "freed").
p4 := p2 p4 := p2
pstate4, err := state.AddProxy(&p4, "fake-token") pstate4, err := state.AddProxy(&p4, "fake-token", "")
require.NoError(err) require.NoError(err)
svc4 := pstate4.Proxy.ProxyService svc4 := pstate4.Proxy.ProxyService
assert.Contains([]int{20000, 20001}, svc2.Port) assert.Contains([]int{20000, 20001}, svc2.Port)
@ -1865,3 +1868,65 @@ func drainCh(ch chan struct{}) {
} }
} }
} }
// Tests the logic for retaining tokens and ports through restore (i.e.
// proxy-service already restored and token passed in externally)
func TestStateProxyRestore(t *testing.T) {
t.Parallel()
state := local.NewState(local.Config{
// Wide random range to make it very unlikely to pass by chance
ProxyBindMinPort: 10000,
ProxyBindMaxPort: 20000,
}, log.New(os.Stderr, "", log.LstdFlags), &token.Store{})
// Stub state syncing
state.TriggerSyncChanges = func() {}
webSvc := structs.NodeService{
Service: "web",
}
p1 := structs.ConnectManagedProxy{
ExecMode: structs.ProxyExecModeDaemon,
Command: []string{"consul", "connect", "proxy"},
TargetServiceID: "web",
}
p2 := p1
require := require.New(t)
assert := assert.New(t)
// Add a target service
require.NoError(state.AddService(&webSvc, "fake-token-web"))
// Add the proxy for first time to get the proper service definition to
// register
pstate, err := state.AddProxy(&p1, "fake-token", "")
require.NoError(err)
// Now start again with a brand new state
state2 := local.NewState(local.Config{
// Wide random range to make it very unlikely to pass by chance
ProxyBindMinPort: 10000,
ProxyBindMaxPort: 20000,
}, log.New(os.Stderr, "", log.LstdFlags), &token.Store{})
// Stub state syncing
state2.TriggerSyncChanges = func() {}
// Register the target service
require.NoError(state2.AddService(&webSvc, "fake-token-web"))
// "Restore" the proxy service
require.NoError(state.AddService(p1.ProxyService, "fake-token-web"))
// Now we can AddProxy with the "restored" token
pstate2, err := state.AddProxy(&p2, "fake-token", pstate.ProxyToken)
require.NoError(err)
// Check it still has the same port and token as before
assert.Equal(pstate.ProxyToken, pstate2.ProxyToken)
assert.Equal(p1.ProxyService.Port, p2.ProxyService.Port)
}

View File

@ -6,6 +6,7 @@ import (
"log" "log"
"os" "os"
"os/exec" "os/exec"
"path"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@ -75,11 +76,9 @@ type Daemon struct {
// second. // second.
pollInterval time.Duration pollInterval time.Duration
// daemonizePath is set only in tests to control the path to the daemonize // daemonizeCmd is set only in tests to control the path and args to the
// command. The test executable itself will not respond to the consul command // daemonize command.
// arguments we need to make this work so we rely on the current source being daemonizeCmd []string
// built and installed here to run the tests.
daemonizePath string
// process is the started process // process is the started process
lock sync.Mutex lock sync.Mutex
@ -112,6 +111,18 @@ func (p *Daemon) Start() error {
p.stopCh = stopCh p.stopCh = stopCh
p.exitedCh = exitedCh p.exitedCh = exitedCh
// Ensure log dirs exist
if p.StdoutPath != "" {
if err := os.MkdirAll(path.Dir(p.StdoutPath), 0755); err != nil {
return err
}
}
if p.StderrPath != "" {
if err := os.MkdirAll(path.Dir(p.StderrPath), 0755); err != nil {
return err
}
}
// Start the loop. // Start the loop.
go p.keepAlive(stopCh, exitedCh) go p.keepAlive(stopCh, exitedCh)
@ -270,19 +281,21 @@ func (p *Daemon) start() (*os.Process, error) {
daemonCmd.Path = dCmd[0] daemonCmd.Path = dCmd[0]
// First arguments are for the stdout, stderr // First arguments are for the stdout, stderr
daemonCmd.Args = append(dCmd, p.StdoutPath) daemonCmd.Args = append(dCmd, p.StdoutPath)
daemonCmd.Args = append(daemonCmd.Args, p.StdoutPath) daemonCmd.Args = append(daemonCmd.Args, p.StderrPath)
daemonCmd.Args = append(daemonCmd.Args, p.Args...) daemonCmd.Args = append(daemonCmd.Args, p.Args...)
daemonCmd.Env = env daemonCmd.Env = env
// setup stdout so we can read the PID // setup stdout so we can read the PID
var out bytes.Buffer var out bytes.Buffer
daemonCmd.Stdout = &out daemonCmd.Stdout = &out
daemonCmd.Stderr = &out
// Run it to completion - it should exit immediately (this calls wait to // Run it to completion - it should exit immediately (this calls wait to
// ensure we don't leave the daemonize command as a zombie) // ensure we don't leave the daemonize command as a zombie)
p.Logger.Printf("[DEBUG] agent/proxy: starting proxy: %q %#v", daemonCmd.Path, p.Logger.Printf("[DEBUG] agent/proxy: starting proxy: %q %#v", daemonCmd.Path,
daemonCmd.Args[1:]) daemonCmd.Args[1:])
if err := daemonCmd.Run(); err != nil { if err := daemonCmd.Run(); err != nil {
p.Logger.Printf("[DEBUG] agent/proxy: daemonize output: %s", out.String())
return nil, err return nil, err
} }
@ -313,15 +326,16 @@ func (p *Daemon) start() (*os.Process, error) {
// daemonizeCommand returns the daemonize command. // daemonizeCommand returns the daemonize command.
func (p *Daemon) daemonizeCommand() ([]string, error) { func (p *Daemon) daemonizeCommand() ([]string, error) {
// test override
if p.daemonizeCmd != nil {
return p.daemonizeCmd, nil
}
// Get the path to the current executable. This is cached once by the // Get the path to the current executable. This is cached once by the
// library so this is effectively just a variable read. // library so this is effectively just a variable read.
execPath, err := os.Executable() execPath, err := os.Executable()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if p.daemonizePath != "" {
execPath = p.daemonizePath
}
return []string{execPath, "connect", "daemonize"}, nil return []string{execPath, "connect", "daemonize"}, nil
} }

View File

@ -102,6 +102,10 @@ type Manager struct {
// proxies (unlikely scenario). // proxies (unlikely scenario).
lastSnapshot *snapshot lastSnapshot *snapshot
// daemonizeCmd is set only in tests to control the path and args to the
// daemonize command.
daemonizeCmd []string
proxies map[string]Proxy proxies map[string]Proxy
} }
@ -405,6 +409,7 @@ func (m *Manager) newProxy(mp *local.ManagedProxy) (Proxy, error) {
proxy.Args = command // idx 0 is path but preserved since it should be proxy.Args = command // idx 0 is path but preserved since it should be
proxy.ProxyId = id proxy.ProxyId = id
proxy.ProxyToken = mp.ProxyToken proxy.ProxyToken = mp.ProxyToken
proxy.daemonizeCmd = m.daemonizeCmd
return proxy, nil return proxy, nil
default: default:

View File

@ -361,6 +361,10 @@ func testManager(t *testing.T) (*Manager, func()) {
td, closer := testTempDir(t) td, closer := testTempDir(t)
m.DataDir = td m.DataDir = td
// Override daemonize command to use the built-in test binary. Note that Args
// includes the binary path as first arg.
m.daemonizeCmd = helperProcess("daemonize").Args
return m, func() { closer() } return m, func() { closer() }
} }
@ -368,8 +372,9 @@ func testManager(t *testing.T) (*Manager, func()) {
// (expected to be from the helperProcess function call). It returns the // (expected to be from the helperProcess function call). It returns the
// ID for deregistration. // ID for deregistration.
func testStateProxy(t *testing.T, state *local.State, service string, cmd *exec.Cmd) string { func testStateProxy(t *testing.T, state *local.State, service string, cmd *exec.Cmd) string {
command := []string{cmd.Path} // Note that exec.Command already ensures the command name is the first
command = append(command, cmd.Args...) // argument in the list so no need to append again
command := cmd.Args
require.NoError(t, state.AddService(&structs.NodeService{ require.NoError(t, state.AddService(&structs.NodeService{
Service: service, Service: service,

View File

@ -1,15 +1,15 @@
package proxy package proxy
import ( import (
"log"
"strings" "strings"
"time" "time"
) )
// isProcessAlreadyFinishedErr does a janky comparison with an error string // isProcessAlreadyFinishedErr does a janky comparison with an error string
// defined in os/exec_unix.go and os/exec_windows.go which we encounter due to races. // defined in os/exec_unix.go and os/exec_windows.go which we encounter due to
// These case tests to fail since Stop returns an error sometimes so we should // races with polling the external process. These case tests to fail since Stop
// notice if this string stops matching the error in a future go version. // returns an error sometimes so we should notice if this string stops matching
// the error in a future go version.
func isProcessAlreadyFinishedErr(err error) bool { func isProcessAlreadyFinishedErr(err error) bool {
return strings.Contains(err.Error(), "os: process already finished") return strings.Contains(err.Error(), "os: process already finished")
} }
@ -31,7 +31,6 @@ func externalWait(pid int, pollInterval time.Duration) (<-chan struct{}, func())
return return
default: default:
} }
log.Printf("checking pid %d", pid)
if _, err := findProcess(pid); err != nil { if _, err := findProcess(pid); err != nil {
close(ch) close(ch)
return return

View File

@ -10,6 +10,9 @@ import (
"strconv" "strconv"
"testing" "testing"
"time" "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 // testLogger is a logger that can be used by tests that require a
@ -55,29 +58,17 @@ func helperProcess(s ...string) *exec.Cmd {
// interactions. The Daemon has it's Path, Args and stdio paths populated but // interactions. The Daemon has it's Path, Args and stdio paths populated but
// other fields might need to be set depending on test requirements. // other fields might need to be set depending on test requirements.
// //
// NOTE: this relies on a sufficiently recent version on consul being installed // This relies on the TestMainInterceptDaemonize hack being active.
// in your path so that the daemonize command can be used. That's gross but hard
// to see how we can do better given that tests are separate binaries and we
// need consul's daemonize mode to work correctly. I considered hacks around
// building the local tree and getting the absolute path to the resulting binary
// but that seems gross in a different way. This is the same or weaker
// assumption our `api` test suit makes already...
func helperProcessDaemon(s ...string) *Daemon { func helperProcessDaemon(s ...string) *Daemon {
cs := []string{os.Args[0], "-test.run=TestHelperProcess", "--", helperProcessSentinel} cs := []string{os.Args[0], "-test.run=TestHelperProcess", "--", helperProcessSentinel}
cs = append(cs, s...) cs = append(cs, s...)
path, err := exec.LookPath("consul")
if err != nil || path == "" {
panic("consul not found on $PATH - download and install " +
"consul or skip this test")
}
return &Daemon{ return &Daemon{
Path: os.Args[0], Path: os.Args[0],
Args: cs, Args: cs,
StdoutPath: "_", // dev null them for now StdoutPath: "_", // dev null them for now
StderrPath: "_", StderrPath: "_",
daemonizePath: path, daemonizeCmd: helperProcess("daemonize").Args,
} }
} }
@ -214,6 +205,24 @@ func TestHelperProcess(t *testing.T) {
time.Sleep(time.Hour) 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: default:
fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd) fmt.Fprintf(os.Stderr, "Unknown command: %q\n", cmd)
os.Exit(2) os.Exit(2)

View File

@ -1140,7 +1140,7 @@ func TestAPI_AgentConnectProxyConfig(t *testing.T) {
Port: 8000, Port: 8000,
Connect: &AgentServiceConnect{ Connect: &AgentServiceConnect{
Proxy: &AgentServiceConnectProxy{ Proxy: &AgentServiceConnectProxy{
Command: []string{"consul connect proxy"}, Command: []string{"consul", "connect", "proxy"},
Config: map[string]interface{}{ Config: map[string]interface{}{
"foo": "bar", "foo": "bar",
}, },
@ -1157,7 +1157,7 @@ func TestAPI_AgentConnectProxyConfig(t *testing.T) {
ProxyServiceID: "foo-proxy", ProxyServiceID: "foo-proxy",
TargetServiceID: "foo", TargetServiceID: "foo",
TargetServiceName: "foo", TargetServiceName: "foo",
ContentHash: "93baee1d838888ae", ContentHash: "2a29f8237db69d0e",
ExecMode: "daemon", ExecMode: "daemon",
Command: []string{"consul", "connect", "proxy"}, Command: []string{"consul", "connect", "proxy"},
Config: map[string]interface{}{ Config: map[string]interface{}{

View File

@ -23,17 +23,15 @@ type cmd struct {
} }
func (c *cmd) Run(args []string) int { func (c *cmd) Run(args []string) int {
// Ignore initial `consul connect daemonize` numArgs := len(args)
offset := 3 if numArgs < 3 {
numArgs := len(os.Args) - offset
if numArgs < 4 {
c.UI.Error("Need at least 3 arguments; stdoutPath, stdinPath, " + c.UI.Error("Need at least 3 arguments; stdoutPath, stdinPath, " +
"executablePath [arguments...]") "executablePath [arguments...]")
os.Exit(1) os.Exit(1)
} }
c.stdoutPath, c.stderrPath = os.Args[offset], os.Args[offset+1] c.stdoutPath, c.stderrPath = args[0], args[1]
c.cmdArgs = os.Args[offset+2:] // includes the executable as arg 0 as expected c.cmdArgs = args[2:] // includes the executable as arg 0 as expected
// Open log files if specified // Open log files if specified
var stdoutF, stderrF *os.File var stdoutF, stderrF *os.File