open-nomad/e2e/execagent/execagent.go
Michael Schurter cd87afd15f e2e: add NomadAgent and basic client state test
The e2e test code is absolutely hideous and leaks processes and files
on disk. NomadAgent seems useful, but the clientstate e2e tests are very
messy and slow. The last test "Corrupt" is probably the most useful as
it explicitly corrupts the state file whereas the other tests attempt to
reproduce steps thought to cause corruption in earlier releases of
Nomad.
2019-03-21 07:14:34 -07:00

265 lines
5.2 KiB
Go

package execagent
import (
"fmt"
"io"
"io/ioutil"
"net"
"os"
"os/exec"
"path/filepath"
"text/template"
"github.com/hashicorp/nomad/api"
)
type AgentMode int
const (
// Conf enum is for configuring either a client, server, or mixed agent.
ModeClient AgentMode = 1
ModeServer AgentMode = 2
ModeBoth = ModeClient | ModeServer
)
func init() {
if d := os.Getenv("NOMAD_TEST_DIR"); d != "" {
BaseDir = d
}
}
var (
// BaseDir is where tests will store state and can be overridden by
// setting NOMAD_TEST_DIR. Defaults to "/opt/nomadtest"
BaseDir = "/opt/nomadtest"
agentTemplate = template.Must(template.New("agent").Parse(`
enable_debug = true
log_level = "{{ or .LogLevel "DEBUG" }}"
ports {
http = {{.HTTP}}
rpc = {{.RPC}}
serf = {{.Serf}}
}
{{ if .EnableServer }}
server {
enabled = true
bootstrap_expect = 1
}
{{ end }}
{{ if .EnableClient }}
client {
enabled = true
options = {
"driver.raw_exec.enable" = "1"
}
}
{{ end }}
`))
)
type AgentTemplateVars struct {
HTTP int
RPC int
Serf int
EnableClient bool
EnableServer bool
LogLevel string
}
func newAgentTemplateVars() (*AgentTemplateVars, error) {
httpPort, err := getFreePort()
if err != nil {
return nil, err
}
rpcPort, err := getFreePort()
if err != nil {
return nil, err
}
serfPort, err := getFreePort()
if err != nil {
return nil, err
}
vars := AgentTemplateVars{
HTTP: httpPort,
RPC: rpcPort,
Serf: serfPort,
}
return &vars, nil
}
func writeConfig(path string, vars *AgentTemplateVars) error {
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return err
}
defer f.Close()
return agentTemplate.Execute(f, vars)
}
// NomadAgent manages an external Nomad agent process.
type NomadAgent struct {
// BinPath is the path to the Nomad binary
BinPath string
// DataDir is the path state will be saved in
DataDir string
// ConfFile is the path to the agent's conf file
ConfFile string
// Cmd is the agent process
Cmd *exec.Cmd
// Vars are the config parameters used to template
Vars *AgentTemplateVars
}
// NewMixedAgent creates a new Nomad agent in mixed server+client mode but does
// not start the agent process until the Start() method is called.
func NewMixedAgent(bin string) (*NomadAgent, error) {
if err := os.MkdirAll(BaseDir, 755); err != nil {
return nil, err
}
dir, err := ioutil.TempDir(BaseDir, "agent")
if err != nil {
return nil, err
}
vars, err := newAgentTemplateVars()
if err != nil {
return nil, err
}
vars.EnableClient = true
vars.EnableServer = true
conf := filepath.Join(dir, "config.hcl")
if err := writeConfig(conf, vars); err != nil {
return nil, err
}
na := &NomadAgent{
BinPath: bin,
DataDir: dir,
ConfFile: conf,
Vars: vars,
Cmd: exec.Command(bin, "agent", "-config", conf, "-data-dir", dir),
}
return na, nil
}
// NewClientServerPair creates a pair of Nomad agents: 1 server, 1 client.
func NewClientServerPair(bin string, serverOut, clientOut io.Writer) (
server *NomadAgent, client *NomadAgent, err error) {
if err := os.MkdirAll(BaseDir, 755); err != nil {
return nil, nil, err
}
sdir, err := ioutil.TempDir(BaseDir, "server")
if err != nil {
return nil, nil, err
}
svars, err := newAgentTemplateVars()
if err != nil {
return nil, nil, err
}
svars.LogLevel = "WARN"
svars.EnableServer = true
sconf := filepath.Join(sdir, "config.hcl")
if err := writeConfig(sconf, svars); err != nil {
return nil, nil, err
}
server = &NomadAgent{
BinPath: bin,
DataDir: sdir,
ConfFile: sconf,
Vars: svars,
Cmd: exec.Command(bin, "agent", "-config", sconf, "-data-dir", sdir),
}
server.Cmd.Stdout = serverOut
server.Cmd.Stderr = serverOut
cdir, err := ioutil.TempDir(BaseDir, "client")
if err != nil {
return nil, nil, err
}
cvars, err := newAgentTemplateVars()
if err != nil {
return nil, nil, err
}
cvars.EnableClient = true
cconf := filepath.Join(cdir, "config.hcl")
if err := writeConfig(cconf, cvars); err != nil {
return nil, nil, err
}
client = &NomadAgent{
BinPath: bin,
DataDir: cdir,
ConfFile: cconf,
Vars: cvars,
Cmd: exec.Command(bin, "agent",
"-config", cconf,
"-data-dir", cdir,
"-servers", fmt.Sprintf("127.0.0.1:%d", svars.RPC),
),
}
client.Cmd.Stdout = clientOut
client.Cmd.Stderr = clientOut
return
}
// Start the agent command.
func (n *NomadAgent) Start() error {
return n.Cmd.Start()
}
// Stop sends an interrupt signal and returns the command's Wait error.
func (n *NomadAgent) Stop() error {
if err := n.Cmd.Process.Signal(os.Interrupt); err != nil {
return err
}
return n.Cmd.Wait()
}
// Destroy stops the agent and removes the data dir.
func (n *NomadAgent) Destroy() error {
if err := n.Stop(); err != nil {
return err
}
return os.RemoveAll(n.DataDir)
}
// Client returns an api.Client for the agent.
func (n *NomadAgent) Client() (*api.Client, error) {
conf := api.DefaultConfig()
conf.Address = fmt.Sprintf("http://127.0.0.1:%d", n.Vars.HTTP)
return api.NewClient(conf)
}
func getFreePort() (int, error) {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return 0, err
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return 0, err
}
defer l.Close()
return l.Addr().(*net.TCPAddr).Port, nil
}