mirror of
https://github.com/kemko/nomad.git
synced 2026-01-02 00:15:43 +03:00
392 lines
8.5 KiB
Go
392 lines
8.5 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package execagent
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"text/template"
|
|
|
|
"github.com/hashicorp/go-hclog"
|
|
"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
|
|
name = "{{ or .AgentName "nomad-e2e-test-agent" }}"
|
|
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
|
|
node_pool = "{{ or .NodePool "default" }}"
|
|
|
|
options = {
|
|
"driver.raw_exec.enable" = "1"
|
|
}
|
|
{{- $retry_join_length := len .RetryJoinAddrs }}{{ if not (eq $retry_join_length 0) }}
|
|
server_join {
|
|
retry_join = [{{ range $index, $element := .RetryJoinAddrs }}{{if $index}}, {{end}}"{{$element}}"{{ end }}]
|
|
}
|
|
{{ end }}
|
|
}
|
|
{{ end }}
|
|
`))
|
|
)
|
|
|
|
type AgentTemplateVars struct {
|
|
HTTP int
|
|
RPC int
|
|
Serf int
|
|
EnableClient bool
|
|
EnableServer bool
|
|
|
|
// AgentName is the name to apply to the Nomad agent. This is optional, but
|
|
// allows for multiple agents to be run on the same host. If not set, it
|
|
// will default to "nomad-e2e-test-agent".
|
|
AgentName string
|
|
|
|
LogLevel string
|
|
|
|
// NodePool is the Nomad node pool to assign the agent to when running with
|
|
// client mode enabled. This will default to the "default" node pool if not
|
|
// set.
|
|
NodePool string
|
|
|
|
// RetryJoinAddrs is a list of addresses to use for the retry_join config
|
|
// block.
|
|
RetryJoinAddrs []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,
|
|
LogLevel: hclog.Warn.String(),
|
|
NodePool: "default",
|
|
}
|
|
|
|
return &vars, nil
|
|
}
|
|
|
|
// SetMode is a helper function to allow setting the agent mode (client, server,
|
|
// or both).
|
|
func (a *AgentTemplateVars) SetMode(mode AgentMode) {
|
|
switch mode {
|
|
case ModeClient:
|
|
a.EnableClient = true
|
|
a.EnableServer = false
|
|
case ModeServer:
|
|
a.EnableClient = false
|
|
a.EnableServer = true
|
|
case ModeBoth:
|
|
a.EnableClient = true
|
|
a.EnableServer = true
|
|
}
|
|
}
|
|
|
|
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, 0755); err != nil {
|
|
return nil, err
|
|
}
|
|
dir, err := os.MkdirTemp(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, 0755); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
sdir, err := os.MkdirTemp(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 := os.MkdirTemp(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
|
|
}
|
|
|
|
// TemplateVariableCallbackFunc is a callback function that allow callers to
|
|
// modify the template variables before the config file is written out.
|
|
type TemplateVariableCallbackFunc func(c *AgentTemplateVars)
|
|
|
|
func NewSingleModeAgent(
|
|
bin, baseDir, additionalConfig string,
|
|
mode AgentMode,
|
|
writer io.Writer,
|
|
varCallbackFn TemplateVariableCallbackFunc,
|
|
) (*NomadAgent, error) {
|
|
|
|
templateVars, err := newAgentTemplateVars()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Allow the caller to modify the template variables before we write out the
|
|
// config file.
|
|
if varCallbackFn != nil {
|
|
varCallbackFn(templateVars)
|
|
}
|
|
|
|
// Set the mode (client, server, or both)
|
|
templateVars.SetMode(mode)
|
|
|
|
baseDataDir := BaseDir
|
|
|
|
if baseDir != "" {
|
|
baseDataDir = baseDir
|
|
}
|
|
|
|
if err := os.MkdirAll(baseDataDir, 0755); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
agentDir, err := os.MkdirTemp(baseDataDir, "agent")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
agentConfig := filepath.Join(agentDir, "agent.hcl")
|
|
if err := writeConfig(agentConfig, templateVars); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
commandArgs := []string{
|
|
"agent",
|
|
"-config=" + agentConfig,
|
|
"-data-dir=" + agentDir,
|
|
}
|
|
|
|
// If the caller specifieed additional config, write it out to a file and
|
|
// add it to the command args.
|
|
//
|
|
// This allows for arbitrary config to be added that isn't supported by the
|
|
// template.
|
|
//
|
|
// The caller is responsible for ensuring the additional config is valid.
|
|
if additionalConfig != "" {
|
|
|
|
extraFilePath := filepath.Join(agentDir, "extra.hcl")
|
|
|
|
if err := os.WriteFile(extraFilePath, []byte(additionalConfig), 0755); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
commandArgs = append(commandArgs, "-config="+extraFilePath)
|
|
}
|
|
|
|
nomadAgent := &NomadAgent{
|
|
BinPath: bin,
|
|
DataDir: agentDir,
|
|
ConfFile: agentConfig,
|
|
Vars: templateVars,
|
|
Cmd: exec.Command(bin, commandArgs...),
|
|
}
|
|
|
|
nomadAgent.Cmd.Stdout = writer
|
|
nomadAgent.Cmd.Stderr = writer
|
|
|
|
return nomadAgent, nil
|
|
}
|
|
|
|
// 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
|
|
}
|