commit
710fcf45e6
17
api/agent.go
17
api/agent.go
|
@ -95,9 +95,10 @@ func (a *Agent) Region() (string, error) {
|
|||
|
||||
// Join is used to instruct a server node to join another server
|
||||
// via the gossip protocol. Multiple addresses may be specified.
|
||||
// We attempt to join all of the hosts in the list. If one or
|
||||
// We attempt to join all of the hosts in the list. Returns the
|
||||
// number of nodes successfully joined and any error. If one or
|
||||
// more nodes have a successful result, no error is returned.
|
||||
func (a *Agent) Join(addrs ...string) error {
|
||||
func (a *Agent) Join(addrs ...string) (int, error) {
|
||||
// Accumulate the addresses
|
||||
v := url.Values{}
|
||||
for _, addr := range addrs {
|
||||
|
@ -108,12 +109,12 @@ func (a *Agent) Join(addrs ...string) error {
|
|||
var resp joinResponse
|
||||
_, err := a.client.write("/v1/agent/join?"+v.Encode(), nil, &resp, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed joining: %s", err)
|
||||
return 0, fmt.Errorf("failed joining: %s", err)
|
||||
}
|
||||
if resp.Error != "" {
|
||||
return fmt.Errorf("failed joining: %s", resp.Error)
|
||||
return 0, fmt.Errorf("failed joining: %s", resp.Error)
|
||||
}
|
||||
return nil
|
||||
return resp.NumJoined, nil
|
||||
}
|
||||
|
||||
// Members is used to query all of the known server members
|
||||
|
@ -137,8 +138,8 @@ func (a *Agent) ForceLeave(node string) error {
|
|||
// joinResponse is used to decode the response we get while
|
||||
// sending a member join request.
|
||||
type joinResponse struct {
|
||||
NumNodes int `json:"num_nodes"`
|
||||
Error string `json:"error"`
|
||||
NumJoined int `json:"num_joined"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// AgentMember represents a cluster member known to the agent
|
||||
|
@ -147,7 +148,7 @@ type AgentMember struct {
|
|||
Addr string
|
||||
Port uint16
|
||||
Tags map[string]string
|
||||
Status int
|
||||
Status string
|
||||
ProtocolMin uint8
|
||||
ProtocolMax uint8
|
||||
ProtocolCur uint8
|
||||
|
|
|
@ -2,6 +2,8 @@ package api
|
|||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
)
|
||||
|
||||
func TestAgent_Self(t *testing.T) {
|
||||
|
@ -59,22 +61,32 @@ func TestAgent_Datacenter(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAgent_Join(t *testing.T) {
|
||||
c, s := makeClient(t, nil, nil)
|
||||
defer s.Stop()
|
||||
a := c.Agent()
|
||||
c1, s1 := makeClient(t, nil, nil)
|
||||
defer s1.Stop()
|
||||
a1 := c1.Agent()
|
||||
|
||||
_, s2 := makeClient(t, nil, func(c *testutil.TestServerConfig) {
|
||||
c.Server.Bootstrap = false
|
||||
})
|
||||
defer s2.Stop()
|
||||
|
||||
// Attempting to join a non-existent host returns error
|
||||
if err := a.Join("nope"); err == nil {
|
||||
n, err := a1.Join("nope")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nothing")
|
||||
}
|
||||
if n != 0 {
|
||||
t.Fatalf("expected 0 nodes, got: %d", n)
|
||||
}
|
||||
|
||||
// TODO(ryanuber): This is pretty much a worthless test,
|
||||
// since we are just joining ourselves. Once the agent
|
||||
// respects config options, change this to actually make
|
||||
// two nodes and join them.
|
||||
if err := a.Join("127.0.0.1"); err != nil {
|
||||
// Returns correctly if join succeeds
|
||||
n, err = a1.Join(s2.SerfAddr)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("expected 1 node, got: %d", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgent_Members(t *testing.T) {
|
||||
|
|
20
api/api.go
20
api/api.go
|
@ -67,8 +67,8 @@ type WriteMeta struct {
|
|||
|
||||
// Config is used to configure the creation of a client
|
||||
type Config struct {
|
||||
// URL is the address of the Nomad agent
|
||||
URL string
|
||||
// Address is the address of the Nomad agent
|
||||
Address string
|
||||
|
||||
// Region to use. If not provided, the default agent region is used.
|
||||
Region string
|
||||
|
@ -85,11 +85,11 @@ type Config struct {
|
|||
// DefaultConfig returns a default configuration for the client
|
||||
func DefaultConfig() *Config {
|
||||
config := &Config{
|
||||
URL: "http://127.0.0.1:4646",
|
||||
Address: "http://127.0.0.1:4646",
|
||||
HttpClient: http.DefaultClient,
|
||||
}
|
||||
if url := os.Getenv("NOMAD_HTTP_URL"); url != "" {
|
||||
config.URL = url
|
||||
if addr := os.Getenv("NOMAD_ADDR"); addr != "" {
|
||||
config.Address = addr
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
@ -104,10 +104,10 @@ func NewClient(config *Config) (*Client, error) {
|
|||
// bootstrap the config
|
||||
defConfig := DefaultConfig()
|
||||
|
||||
if config.URL == "" {
|
||||
config.URL = defConfig.URL
|
||||
} else if _, err := url.Parse(config.URL); err != nil {
|
||||
return nil, fmt.Errorf("invalid url '%s': %v", config.URL, err)
|
||||
if config.Address == "" {
|
||||
config.Address = defConfig.Address
|
||||
} else if _, err := url.Parse(config.Address); err != nil {
|
||||
return nil, fmt.Errorf("invalid address '%s': %v", config.Address, err)
|
||||
}
|
||||
|
||||
if config.HttpClient == nil {
|
||||
|
@ -194,7 +194,7 @@ func (r *request) toHTTP() (*http.Request, error) {
|
|||
|
||||
// newRequest is used to create a new request
|
||||
func (c *Client) newRequest(method, path string) *request {
|
||||
base, _ := url.Parse(c.config.URL)
|
||||
base, _ := url.Parse(c.config.Address)
|
||||
u, _ := url.Parse(path)
|
||||
r := &request{
|
||||
config: &c.config,
|
||||
|
|
|
@ -11,10 +11,21 @@ import (
|
|||
|
||||
type configCallback func(c *Config)
|
||||
|
||||
// seen is used to track which tests we have already marked as parallel
|
||||
var seen map[*testing.T]struct{}
|
||||
|
||||
func init() {
|
||||
seen = make(map[*testing.T]struct{})
|
||||
}
|
||||
|
||||
func makeClient(t *testing.T, cb1 configCallback,
|
||||
cb2 testutil.ServerConfigCallback) (*Client, *testutil.TestServer) {
|
||||
// Always run these tests in parallel
|
||||
t.Parallel()
|
||||
// Always run these tests in parallel. Check if we have already
|
||||
// marked the current test, as more than 1 call causes panics.
|
||||
if _, ok := seen[t]; !ok {
|
||||
seen[t] = struct{}{}
|
||||
t.Parallel()
|
||||
}
|
||||
|
||||
// Make client config
|
||||
conf := DefaultConfig()
|
||||
|
@ -24,7 +35,7 @@ func makeClient(t *testing.T, cb1 configCallback,
|
|||
|
||||
// Create server
|
||||
server := testutil.NewTestServer(t, cb2)
|
||||
conf.URL = "http://" + server.HTTPAddr
|
||||
conf.Address = "http://" + server.HTTPAddr
|
||||
|
||||
// Create client
|
||||
client, err := NewClient(conf)
|
||||
|
@ -39,13 +50,13 @@ func TestDefaultConfig_env(t *testing.T) {
|
|||
t.Parallel()
|
||||
url := "http://1.2.3.4:5678"
|
||||
|
||||
os.Setenv("NOMAD_HTTP_URL", url)
|
||||
defer os.Setenv("NOMAD_HTTP_URL", "")
|
||||
os.Setenv("NOMAD_ADDR", url)
|
||||
defer os.Setenv("NOMAD_ADDR", "")
|
||||
|
||||
config := DefaultConfig()
|
||||
|
||||
if config.URL != url {
|
||||
t.Errorf("expected %q to be %q", config.URL, url)
|
||||
if config.Address != url {
|
||||
t.Errorf("expected %q to be %q", config.Address, url)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,27 +1,57 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/serf/serf"
|
||||
)
|
||||
|
||||
type Member struct {
|
||||
Name string
|
||||
Addr net.IP
|
||||
Port uint16
|
||||
Tags map[string]string
|
||||
Status string
|
||||
ProtocolMin uint8
|
||||
ProtocolMax uint8
|
||||
ProtocolCur uint8
|
||||
DelegateMin uint8
|
||||
DelegateMax uint8
|
||||
DelegateCur uint8
|
||||
}
|
||||
|
||||
func nomadMember(m serf.Member) Member {
|
||||
return Member{
|
||||
Name: m.Name,
|
||||
Addr: m.Addr,
|
||||
Port: m.Port,
|
||||
Tags: m.Tags,
|
||||
Status: m.Status.String(),
|
||||
ProtocolMin: m.ProtocolMin,
|
||||
ProtocolMax: m.ProtocolMax,
|
||||
ProtocolCur: m.ProtocolCur,
|
||||
DelegateMin: m.DelegateMin,
|
||||
DelegateMax: m.DelegateMax,
|
||||
DelegateCur: m.DelegateCur,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPServer) AgentSelfRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
if req.Method != "GET" {
|
||||
return nil, CodedError(405, ErrInvalidMethod)
|
||||
}
|
||||
|
||||
// Get the member as a server
|
||||
var member *serf.Member
|
||||
var member serf.Member
|
||||
srv := s.agent.Server()
|
||||
if srv != nil {
|
||||
mem := srv.LocalMember()
|
||||
member = &mem
|
||||
member = srv.LocalMember()
|
||||
}
|
||||
|
||||
self := agentSelf{
|
||||
Config: s.agent.config,
|
||||
Member: member,
|
||||
Member: nomadMember(member),
|
||||
Stats: s.agent.Stats(),
|
||||
}
|
||||
return self, nil
|
||||
|
@ -60,7 +90,13 @@ func (s *HTTPServer) AgentMembersRequest(resp http.ResponseWriter, req *http.Req
|
|||
if srv == nil {
|
||||
return nil, CodedError(501, ErrInvalidMethod)
|
||||
}
|
||||
return srv.Members(), nil
|
||||
|
||||
serfMembers := srv.Members()
|
||||
members := make([]Member, len(serfMembers))
|
||||
for i, mem := range serfMembers {
|
||||
members[i] = nomadMember(mem)
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) AgentForceLeaveRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
|
@ -85,7 +121,7 @@ func (s *HTTPServer) AgentForceLeaveRequest(resp http.ResponseWriter, req *http.
|
|||
|
||||
type agentSelf struct {
|
||||
Config *Config `json:"config"`
|
||||
Member *serf.Member `json:"member,omitempty"`
|
||||
Member Member `json:"member,omitempty"`
|
||||
Stats map[string]map[string]string `json:"stats"`
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/serf/serf"
|
||||
)
|
||||
|
||||
func TestHTTP_AgentSelf(t *testing.T) {
|
||||
|
@ -85,7 +83,7 @@ func TestHTTP_AgentMembers(t *testing.T) {
|
|||
}
|
||||
|
||||
// Check the job
|
||||
members := obj.([]serf.Member)
|
||||
members := obj.([]Member)
|
||||
if len(members) != 1 {
|
||||
t.Fatalf("bad: %#v", members)
|
||||
}
|
||||
|
|
60
command/agent_force_leave.go
Normal file
60
command/agent_force_leave.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AgentForceLeaveCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *AgentForceLeaveCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad agent-force-leave [options] <node>
|
||||
|
||||
Forces an agent to enter the "left" state. This can be used to
|
||||
eject nodes which have failed and will not rejoin the cluster.
|
||||
Note that if the member is actually still alive, it will
|
||||
eventually rejoin the cluster again.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *AgentForceLeaveCommand) Synopsis() string {
|
||||
return "Force a member into the 'left' state"
|
||||
}
|
||||
|
||||
func (c *AgentForceLeaveCommand) Run(args []string) int {
|
||||
flags := c.Meta.FlagSet("force-leave", FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we got exactly one node
|
||||
args = flags.Args()
|
||||
if len(args) != 1 {
|
||||
c.Ui.Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
node := args[0]
|
||||
|
||||
// Get the HTTP client
|
||||
client, err := c.Meta.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Call force-leave on the node
|
||||
if err := client.Agent().ForceLeave(node); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error force-leaving node %s: %s", node, err))
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
11
command/agent_force_leave_test.go
Normal file
11
command/agent_force_leave_test.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestAgentForceLeaveCommand_Implements(t *testing.T) {
|
||||
var _ cli.Command = &AgentForceLeaveCommand{}
|
||||
}
|
68
command/agent_info.go
Normal file
68
command/agent_info.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AgentInfoCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *AgentInfoCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad agent-info [options]
|
||||
|
||||
Display status information about the local agent.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *AgentInfoCommand) Synopsis() string {
|
||||
return "Display status information about the local agent"
|
||||
}
|
||||
|
||||
func (c *AgentInfoCommand) Run(args []string) int {
|
||||
flags := c.Meta.FlagSet("agent-info", FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we either got no jobs or exactly one.
|
||||
args = flags.Args()
|
||||
if len(args) > 0 {
|
||||
c.Ui.Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Get the HTTP client
|
||||
client, err := c.Meta.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Query the agent info
|
||||
info, err := client.Agent().Self()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error querying agent info: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
var stats map[string]interface{}
|
||||
stats, _ = info["stats"]
|
||||
|
||||
for section, data := range stats {
|
||||
c.Ui.Output(section)
|
||||
d, _ := data.(map[string]interface{})
|
||||
for k, v := range d {
|
||||
c.Ui.Output(fmt.Sprintf(" %s = %v", k, v))
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
47
command/agent_info_test.go
Normal file
47
command/agent_info_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestAgentInfoCommand_Implements(t *testing.T) {
|
||||
var _ cli.Command = &AgentInfoCommand{}
|
||||
}
|
||||
|
||||
func TestAgentInfoCommand_Run(t *testing.T) {
|
||||
srv, _, url := testServer(t)
|
||||
defer srv.Stop()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
cmd := &AgentInfoCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
code := cmd.Run([]string{"-address=" + url})
|
||||
if code != 0 {
|
||||
t.Fatalf("expected exit 0, got: %d %s", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentInfoCommand_Fails(t *testing.T) {
|
||||
ui := new(cli.MockUi)
|
||||
cmd := &AgentInfoCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Fails on misuse
|
||||
if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, cmd.Help()) {
|
||||
t.Fatalf("expected help output, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Fails on connection failure
|
||||
if code := cmd.Run([]string{"-address=nope"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying agent info") {
|
||||
t.Fatalf("expected failed query error, got: %s", out)
|
||||
}
|
||||
}
|
64
command/agent_join.go
Normal file
64
command/agent_join.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AgentJoinCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *AgentJoinCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad agent-join [options] <addr> [<addr>...]
|
||||
|
||||
Joins the local server to one or more Nomad servers. Joining is
|
||||
only required for server nodes, and only needs to succeed
|
||||
against one or more of the provided addresses. Once joined, the
|
||||
gossip layer will handle discovery of the other server nodes in
|
||||
the cluster.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *AgentJoinCommand) Synopsis() string {
|
||||
return "Join server nodes together"
|
||||
}
|
||||
|
||||
func (c *AgentJoinCommand) Run(args []string) int {
|
||||
flags := c.Meta.FlagSet("agent-join", FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we got at least one node
|
||||
args = flags.Args()
|
||||
if len(args) < 1 {
|
||||
c.Ui.Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
nodes := args
|
||||
|
||||
// Get the HTTP client
|
||||
client, err := c.Meta.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Attempt the join
|
||||
n, err := client.Agent().Join(nodes...)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error joining: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Success
|
||||
c.Ui.Output(fmt.Sprintf("Joined %d nodes successfully", n))
|
||||
return 0
|
||||
}
|
11
command/agent_join_test.go
Normal file
11
command/agent_join_test.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestAgentJoinCommand_Implements(t *testing.T) {
|
||||
var _ cli.Command = &AgentJoinCommand{}
|
||||
}
|
121
command/agent_members.go
Normal file
121
command/agent_members.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
type AgentMembersCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *AgentMembersCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad agent-members [options]
|
||||
|
||||
Display a list of the known members and their status.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage() + `
|
||||
|
||||
Agent Members Options:
|
||||
|
||||
-detailed
|
||||
Show detailed information about each member. This dumps
|
||||
a raw set of tags which shows more information than the
|
||||
default output format.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *AgentMembersCommand) Synopsis() string {
|
||||
return "Display a list of known members and their status"
|
||||
}
|
||||
|
||||
func (c *AgentMembersCommand) Run(args []string) int {
|
||||
var detailed bool
|
||||
|
||||
flags := c.Meta.FlagSet("agent-members", FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
flags.BoolVar(&detailed, "detailed", false, "Show detailed output")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check for extra arguments
|
||||
args = flags.Args()
|
||||
if len(args) != 0 {
|
||||
c.Ui.Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Get the HTTP client
|
||||
client, err := c.Meta.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Query the members
|
||||
mem, err := client.Agent().Members()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error querying members: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Format the list
|
||||
var out []string
|
||||
if detailed {
|
||||
out = detailedOutput(mem)
|
||||
} else {
|
||||
out = standardOutput(mem)
|
||||
}
|
||||
|
||||
// Dump the list
|
||||
c.Ui.Output(columnize.SimpleFormat(out))
|
||||
return 0
|
||||
}
|
||||
|
||||
func standardOutput(mem []*api.AgentMember) []string {
|
||||
// Format the members list
|
||||
members := make([]string, len(mem)+1)
|
||||
members[0] = "Name|Addr|Port|Status|Proto|Build|DC|Region"
|
||||
for i, member := range mem {
|
||||
members[i+1] = fmt.Sprintf("%s|%s|%d|%s|%d|%s|%s|%s",
|
||||
member.Name,
|
||||
member.Addr,
|
||||
member.Port,
|
||||
member.Status,
|
||||
member.ProtocolCur,
|
||||
member.Tags["build"],
|
||||
member.Tags["dc"],
|
||||
member.Tags["region"])
|
||||
}
|
||||
return members
|
||||
}
|
||||
|
||||
func detailedOutput(mem []*api.AgentMember) []string {
|
||||
// Format the members list
|
||||
members := make([]string, len(mem)+1)
|
||||
members[0] = "Name|Addr|Port|Tags"
|
||||
for i, member := range mem {
|
||||
// Format the tags
|
||||
tagPairs := make([]string, 0, len(member.Tags))
|
||||
for k, v := range member.Tags {
|
||||
tagPairs = append(tagPairs, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
tags := strings.Join(tagPairs, ",")
|
||||
|
||||
members[i+1] = fmt.Sprintf("%s|%s|%d|%s",
|
||||
member.Name,
|
||||
member.Addr,
|
||||
member.Port,
|
||||
tags)
|
||||
}
|
||||
return members
|
||||
}
|
65
command/agent_members_test.go
Normal file
65
command/agent_members_test.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestAgentMembersCommand_Implements(t *testing.T) {
|
||||
var _ cli.Command = &AgentMembersCommand{}
|
||||
}
|
||||
|
||||
func TestAgentMembersCommand_Run(t *testing.T) {
|
||||
srv, client, url := testServer(t)
|
||||
defer srv.Stop()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
cmd := &AgentMembersCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Get our own node name
|
||||
name, err := client.Agent().NodeName()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Query the members
|
||||
if code := cmd.Run([]string{"-address=" + url}); code != 0 {
|
||||
t.Fatalf("expected exit 0, got: %d", code)
|
||||
}
|
||||
if out := ui.OutputWriter.String(); !strings.Contains(out, name) {
|
||||
t.Fatalf("expected %q in output, got: %s", name, out)
|
||||
}
|
||||
ui.OutputWriter.Reset()
|
||||
|
||||
// Query members with detailed output
|
||||
if code := cmd.Run([]string{"-address=" + url, "-detailed"}); code != 0 {
|
||||
t.Fatalf("expected exit 0, got: %d", code)
|
||||
}
|
||||
if out := ui.OutputWriter.String(); !strings.Contains(out, "Tags") {
|
||||
t.Fatalf("expected tags in output, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMembersCommand_Fails(t *testing.T) {
|
||||
ui := new(cli.MockUi)
|
||||
cmd := &AgentMembersCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Fails on misuse
|
||||
if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, cmd.Help()) {
|
||||
t.Fatalf("expected help output, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Fails on connection failure
|
||||
if code := cmd.Run([]string{"-address=nope"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying members") {
|
||||
t.Fatalf("expected failed query error, got: %s", out)
|
||||
}
|
||||
}
|
|
@ -4,18 +4,27 @@ import (
|
|||
"bufio"
|
||||
"flag"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
// Names of environment variables used to supply various
|
||||
// config options to the Nomad CLI.
|
||||
EnvNomadAddress = "NOMAD_ADDR"
|
||||
)
|
||||
|
||||
// FlagSetFlags is an enum to define what flags are present in the
|
||||
// default FlagSet returned by Meta.FlagSet.
|
||||
type FlagSetFlags uint
|
||||
|
||||
const (
|
||||
FlagSetNone FlagSetFlags = 0
|
||||
FlagSetServer FlagSetFlags = 1 << iota
|
||||
FlagSetDefault = FlagSetServer
|
||||
FlagSetClient FlagSetFlags = 1 << iota
|
||||
FlagSetDefault = FlagSetClient
|
||||
)
|
||||
|
||||
// Meta contains the meta-options and functionality that nearly every
|
||||
|
@ -34,9 +43,9 @@ type Meta struct {
|
|||
func (m *Meta) FlagSet(n string, fs FlagSetFlags) *flag.FlagSet {
|
||||
f := flag.NewFlagSet(n, flag.ContinueOnError)
|
||||
|
||||
// FlagSetServer tells us to enable the settings for selecting
|
||||
// the server information.
|
||||
if fs&FlagSetServer != 0 {
|
||||
// FlagSetClient is used to enable the settings for specifying
|
||||
// client connectivity options.
|
||||
if fs&FlagSetClient != 0 {
|
||||
f.StringVar(&m.flagAddress, "address", "", "")
|
||||
}
|
||||
|
||||
|
@ -55,3 +64,27 @@ func (m *Meta) FlagSet(n string, fs FlagSetFlags) *flag.FlagSet {
|
|||
|
||||
return f
|
||||
}
|
||||
|
||||
// Client is used to initialize and return a new API client using
|
||||
// the default command line arguments and env vars.
|
||||
func (m *Meta) Client() (*api.Client, error) {
|
||||
config := api.DefaultConfig()
|
||||
if v := os.Getenv(EnvNomadAddress); v != "" {
|
||||
config.Address = v
|
||||
}
|
||||
if m.flagAddress != "" {
|
||||
config.Address = m.flagAddress
|
||||
}
|
||||
return api.NewClient(config)
|
||||
}
|
||||
|
||||
// generalOptionsUsage returns the help string for the global options.
|
||||
func generalOptionsUsage() string {
|
||||
helpText := `
|
||||
-address=<addr>
|
||||
The address of the Nomad server.
|
||||
Overrides the NOMAD_ADDR environment variable if set.
|
||||
Default = http://127.0.0.1:4646
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestFlagSet(t *testing.T) {
|
||||
func TestMeta_FlagSet(t *testing.T) {
|
||||
cases := []struct {
|
||||
Flags FlagSetFlags
|
||||
Expected []string
|
||||
|
@ -17,7 +17,7 @@ func TestFlagSet(t *testing.T) {
|
|||
[]string{},
|
||||
},
|
||||
{
|
||||
FlagSetServer,
|
||||
FlagSetClient,
|
||||
[]string{"address"},
|
||||
},
|
||||
}
|
||||
|
|
77
command/node_drain.go
Normal file
77
command/node_drain.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type NodeDrainCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *NodeDrainCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad node-drain [options] <node>
|
||||
|
||||
Toggles node draining on a specified node. It is required
|
||||
that either -enable or -disable is specified, but not both.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage() + `
|
||||
|
||||
Node Drain Options:
|
||||
|
||||
-disable
|
||||
Disable draining for the specified node.
|
||||
|
||||
-enable
|
||||
Enable draining for the specified node.
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *NodeDrainCommand) Synopsis() string {
|
||||
return "Toggle drain mode on a given node"
|
||||
}
|
||||
|
||||
func (c *NodeDrainCommand) Run(args []string) int {
|
||||
var enable, disable bool
|
||||
|
||||
flags := c.Meta.FlagSet("node-drain", FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
flags.BoolVar(&enable, "enable", false, "Enable drain mode")
|
||||
flags.BoolVar(&disable, "disable", false, "Disable drain mode")
|
||||
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we got either enable or disable, but not both.
|
||||
if (enable && disable) || (!enable && !disable) {
|
||||
c.Ui.Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we got a node ID
|
||||
args = flags.Args()
|
||||
if len(args) != 1 {
|
||||
c.Ui.Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
nodeID := args[0]
|
||||
|
||||
// Get the HTTP client
|
||||
client, err := c.Meta.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Toggle node draining
|
||||
if _, err := client.Nodes().ToggleDrain(nodeID, enable, nil); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error toggling drain mode: %s", err))
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
64
command/node_drain_test.go
Normal file
64
command/node_drain_test.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestNodeDrainCommand_Implements(t *testing.T) {
|
||||
var _ cli.Command = &NodeDrainCommand{}
|
||||
}
|
||||
|
||||
func TestNodeDrainCommand_Fails(t *testing.T) {
|
||||
srv, _, url := testServer(t)
|
||||
defer srv.Stop()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
cmd := &NodeDrainCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Fails on misuse
|
||||
if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, cmd.Help()) {
|
||||
t.Fatalf("expected help output, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Fails on connection failure
|
||||
if code := cmd.Run([]string{"-address=nope", "-enable", "nope"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error toggling") {
|
||||
t.Fatalf("expected failed toggle error, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Fails on non-existent node
|
||||
if code := cmd.Run([]string{"-address=" + url, "-enable", "nope"}); code != 1 {
|
||||
t.Fatalf("expected exit 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "not found") {
|
||||
t.Fatalf("expected not exist error, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Fails if both enable and disable specified
|
||||
if code := cmd.Run([]string{"-enable", "-disable", "nope"}); code != 1 {
|
||||
t.Fatalf("expected exit 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, cmd.Help()) {
|
||||
t.Fatalf("expected help output, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Fails if neither enable or disable specified
|
||||
if code := cmd.Run([]string{"nope"}); code != 1 {
|
||||
t.Fatalf("expected exit 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, cmd.Help()) {
|
||||
t.Fatalf("expected help output, got: %s", out)
|
||||
}
|
||||
}
|
110
command/node_status.go
Normal file
110
command/node_status.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
type NodeStatusCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *NodeStatusCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad node-status [options] [node]
|
||||
|
||||
Display status information about a given node. The list of nodes
|
||||
returned includes only nodes which jobs may be scheduled to, and
|
||||
includes status and other high-level information.
|
||||
|
||||
If a node ID is passed, information for that specific node will
|
||||
be displayed. If no node ID's are passed, then a short-hand
|
||||
list of all nodes will be displayed.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *NodeStatusCommand) Synopsis() string {
|
||||
return "Display status information about nodes"
|
||||
}
|
||||
|
||||
func (c *NodeStatusCommand) Run(args []string) int {
|
||||
flags := c.Meta.FlagSet("node-status", FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we got either a single node or none
|
||||
args = flags.Args()
|
||||
if len(args) > 1 {
|
||||
c.Ui.Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Get the HTTP client
|
||||
client, err := c.Meta.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Use list mode if no node name was provided
|
||||
if len(args) == 0 {
|
||||
// Query the node info
|
||||
nodes, _, err := client.Nodes().List(nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error querying node status: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Return nothing if no nodes found
|
||||
if len(nodes) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Format the nodes list
|
||||
out := make([]string, len(nodes)+1)
|
||||
out[0] = "ID|DC|Name|Class|Drain|Status"
|
||||
for i, node := range nodes {
|
||||
out[i+1] = fmt.Sprintf("%s|%s|%s|%s|%v|%s",
|
||||
node.ID,
|
||||
node.Datacenter,
|
||||
node.Name,
|
||||
node.NodeClass,
|
||||
node.Drain,
|
||||
node.Status)
|
||||
}
|
||||
|
||||
// Dump the output
|
||||
c.Ui.Output(columnize.SimpleFormat(out))
|
||||
return 0
|
||||
}
|
||||
|
||||
// Query the specific node
|
||||
nodeID := args[0]
|
||||
node, _, err := client.Nodes().Info(nodeID, nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error querying node info: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Format the output
|
||||
out := []string{
|
||||
fmt.Sprintf("ID | %s", node.ID),
|
||||
fmt.Sprintf("Name | %s", node.Name),
|
||||
fmt.Sprintf("Class | %s", node.NodeClass),
|
||||
fmt.Sprintf("Datacenter | %s", node.Datacenter),
|
||||
fmt.Sprintf("Drain | %v", node.Drain),
|
||||
fmt.Sprintf("Status | %s", node.Status),
|
||||
}
|
||||
|
||||
// Dump the output
|
||||
c.Ui.Output(columnize.SimpleFormat(out))
|
||||
return 0
|
||||
}
|
63
command/node_status_test.go
Normal file
63
command/node_status_test.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestNodeStatusCommand_Implements(t *testing.T) {
|
||||
var _ cli.Command = &NodeStatusCommand{}
|
||||
}
|
||||
|
||||
func TestNodeStatusCommand_Run(t *testing.T) {
|
||||
srv, _, url := testServer(t)
|
||||
defer srv.Stop()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
cmd := &NodeStatusCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Query all node statuses
|
||||
if code := cmd.Run([]string{"-address=" + url}); code != 0 {
|
||||
t.Fatalf("expected exit 0, got: %d", code)
|
||||
}
|
||||
|
||||
// Expect empty output since we have no nodes
|
||||
if out := ui.OutputWriter.String(); out != "<nil>" {
|
||||
t.Fatalf("expected empty output, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeStatusCommand_Fails(t *testing.T) {
|
||||
srv, _, url := testServer(t)
|
||||
defer srv.Stop()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
cmd := &NodeStatusCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Fails on misuse
|
||||
if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, cmd.Help()) {
|
||||
t.Fatalf("expected help output, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Fails on connection failure
|
||||
if code := cmd.Run([]string{"-address=nope"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying node status") {
|
||||
t.Fatalf("expected failed query error, got: %s", out)
|
||||
}
|
||||
|
||||
// Fails on non-existent node
|
||||
if code := cmd.Run([]string{"-address=" + url, "nope"}); code != 1 {
|
||||
t.Fatalf("expected exit 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "not found") {
|
||||
t.Fatalf("expected not found error, got: %s", out)
|
||||
}
|
||||
}
|
99
command/status.go
Normal file
99
command/status.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
type StatusCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: nomad status [options] [job]
|
||||
|
||||
Display status information about jobs. If no job ID is given,
|
||||
a list of all known jobs will be dumped.
|
||||
|
||||
General Options:
|
||||
|
||||
` + generalOptionsUsage()
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Synopsis() string {
|
||||
return "Display status information about jobs"
|
||||
}
|
||||
|
||||
func (c *StatusCommand) Run(args []string) int {
|
||||
flags := c.Meta.FlagSet("status", FlagSetClient)
|
||||
flags.Usage = func() { c.Ui.Output(c.Help()) }
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check that we either got no jobs or exactly one.
|
||||
args = flags.Args()
|
||||
if len(args) > 1 {
|
||||
c.Ui.Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Get the HTTP client
|
||||
client, err := c.Meta.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Invoke list mode if no job ID.
|
||||
if len(args) == 0 {
|
||||
jobs, _, err := client.Jobs().List(nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error querying jobs: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// No output if we have no jobs
|
||||
if len(jobs) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
out := make([]string, len(jobs)+1)
|
||||
out[0] = "ID|Type|Priority|Status"
|
||||
for i, job := range jobs {
|
||||
out[i+1] = fmt.Sprintf("%s|%s|%d|%s",
|
||||
job.ID,
|
||||
job.Type,
|
||||
job.Priority,
|
||||
job.Status)
|
||||
}
|
||||
c.Ui.Output(columnize.SimpleFormat(out))
|
||||
return 0
|
||||
}
|
||||
|
||||
// Try querying the job
|
||||
jobID := args[0]
|
||||
job, _, err := client.Jobs().Info(jobID, nil)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf("Error querying job: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Format the job info
|
||||
basic := []string{
|
||||
fmt.Sprintf("ID | %s", job.ID),
|
||||
fmt.Sprintf("Name | %s", job.Name),
|
||||
fmt.Sprintf("Type | %s", job.Type),
|
||||
fmt.Sprintf("Priority | %d", job.Priority),
|
||||
fmt.Sprintf("Datacenters | %s", strings.Join(job.Datacenters, ",")),
|
||||
fmt.Sprintf("Status | %s", job.Status),
|
||||
fmt.Sprintf("StatusDescription | %s", job.StatusDescription),
|
||||
}
|
||||
c.Ui.Output(columnize.SimpleFormat(basic))
|
||||
|
||||
return 0
|
||||
}
|
84
command/status_test.go
Normal file
84
command/status_test.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestStatusCommand_Implements(t *testing.T) {
|
||||
var _ cli.Command = &StatusCommand{}
|
||||
}
|
||||
|
||||
func TestStatusCommand_Run(t *testing.T) {
|
||||
srv, client, url := testServer(t)
|
||||
defer srv.Stop()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
cmd := &StatusCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Should return blank for no jobs
|
||||
if code := cmd.Run([]string{"-address=" + url}); code != 0 {
|
||||
t.Fatalf("expected exit 0, got: %d", code)
|
||||
}
|
||||
|
||||
// Check for this awkward nil string, since a nil bytes.Buffer
|
||||
// returns this purposely, and mitchellh/cli has a nil pointer
|
||||
// if nothing was ever output.
|
||||
if out := ui.OutputWriter.String(); out != "<nil>" {
|
||||
t.Fatalf("expected empty output, got: %s", out)
|
||||
}
|
||||
|
||||
// Register two jobs
|
||||
job1 := api.NewBatchJob("job1", "myjob", 1)
|
||||
if _, _, err := client.Jobs().Register(job1, nil); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
job2 := api.NewBatchJob("job2", "myjob", 1)
|
||||
if _, _, err := client.Jobs().Register(job2, nil); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Query again and check the result
|
||||
if code := cmd.Run([]string{"-address=" + url}); code != 0 {
|
||||
t.Fatalf("expected exit 0, got: %d", code)
|
||||
}
|
||||
out := ui.OutputWriter.String()
|
||||
if !strings.Contains(out, "job1") || !strings.Contains(out, "job2") {
|
||||
t.Fatalf("expected job1 and job2, got: %s", out)
|
||||
}
|
||||
ui.OutputWriter.Reset()
|
||||
|
||||
// Query a single job
|
||||
if code := cmd.Run([]string{"-address=" + url, "job2"}); code != 0 {
|
||||
t.Fatalf("expected exit 0, got: %d", code)
|
||||
}
|
||||
out = ui.OutputWriter.String()
|
||||
if strings.Contains(out, "job1") || !strings.Contains(out, "job2") {
|
||||
t.Fatalf("expected only job2, got: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusCommand_Fails(t *testing.T) {
|
||||
ui := new(cli.MockUi)
|
||||
cmd := &StatusCommand{Meta: Meta{Ui: ui}}
|
||||
|
||||
// Fails on misuse
|
||||
if code := cmd.Run([]string{"some", "bad", "args"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, cmd.Help()) {
|
||||
t.Fatalf("expected help output, got: %s", out)
|
||||
}
|
||||
ui.ErrorWriter.Reset()
|
||||
|
||||
// Fails on connection failure
|
||||
if code := cmd.Run([]string{"-address=nope"}); code != 1 {
|
||||
t.Fatalf("expected exit code 1, got: %d", code)
|
||||
}
|
||||
if out := ui.ErrorWriter.String(); !strings.Contains(out, "Error querying jobs") {
|
||||
t.Fatalf("expected failed query error, got: %s", out)
|
||||
}
|
||||
}
|
36
command/util_test.go
Normal file
36
command/util_test.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/testutil"
|
||||
)
|
||||
|
||||
// seen is used to track which tests we have already
|
||||
// marked as parallel. Marking twice causes panic.
|
||||
var seen map[*testing.T]struct{}
|
||||
|
||||
func init() {
|
||||
seen = make(map[*testing.T]struct{})
|
||||
}
|
||||
|
||||
func testServer(t *testing.T) (*testutil.TestServer, *api.Client, string) {
|
||||
// Always run these tests in parallel.
|
||||
if _, ok := seen[t]; !ok {
|
||||
seen[t] = struct{}{}
|
||||
t.Parallel()
|
||||
}
|
||||
|
||||
// Make a new test server
|
||||
srv := testutil.NewTestServer(t, nil)
|
||||
|
||||
// Make a client
|
||||
clientConf := api.DefaultConfig()
|
||||
clientConf.Address = "http://" + srv.HTTPAddr
|
||||
client, err := api.NewClient(clientConf)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
return srv, client, clientConf.Address
|
||||
}
|
42
commands.go
42
commands.go
|
@ -34,6 +34,48 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
|
|||
}, nil
|
||||
},
|
||||
|
||||
"agent-force-leave": func() (cli.Command, error) {
|
||||
return &command.AgentForceLeaveCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"agent-info": func() (cli.Command, error) {
|
||||
return &command.AgentInfoCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"agent-join": func() (cli.Command, error) {
|
||||
return &command.AgentJoinCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"agent-members": func() (cli.Command, error) {
|
||||
return &command.AgentMembersCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"node-drain": func() (cli.Command, error) {
|
||||
return &command.NodeDrainCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"node-status": func() (cli.Command, error) {
|
||||
return &command.NodeStatusCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"status": func() (cli.Command, error) {
|
||||
return &command.StatusCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"version": func() (cli.Command, error) {
|
||||
ver := Version
|
||||
rel := VersionPrerelease
|
||||
|
|
|
@ -29,7 +29,7 @@ var offset uint64
|
|||
|
||||
// TestServerConfig is the main server configuration struct.
|
||||
type TestServerConfig struct {
|
||||
Bootstrap bool `json:"bootstrap,omitempty"`
|
||||
NodeName string `json:"name,omitempty"`
|
||||
DataDir string `json:"data_dir,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
DisableCheckpoint bool `json:"disable_update_check"`
|
||||
|
@ -69,8 +69,8 @@ func defaultServerConfig() *TestServerConfig {
|
|||
idx := int(atomic.AddUint64(&offset, 1))
|
||||
|
||||
return &TestServerConfig{
|
||||
NodeName: fmt.Sprintf("node%d", idx),
|
||||
DisableCheckpoint: true,
|
||||
Bootstrap: true,
|
||||
LogLevel: "DEBUG",
|
||||
Ports: &PortsConfig{
|
||||
HTTP: 20000 + idx,
|
||||
|
@ -170,7 +170,7 @@ func NewTestServer(t *testing.T, cb ServerConfigCallback) *TestServer {
|
|||
}
|
||||
|
||||
// Wait for the server to be ready
|
||||
if nomadConfig.Bootstrap {
|
||||
if nomadConfig.Server.Enabled && nomadConfig.Server.Bootstrap {
|
||||
server.waitForLeader()
|
||||
} else {
|
||||
server.waitForAPI()
|
||||
|
@ -194,7 +194,7 @@ func (s *TestServer) Stop() {
|
|||
// but will likely return before a leader is elected.
|
||||
func (s *TestServer) waitForAPI() {
|
||||
WaitForResult(func() (bool, error) {
|
||||
resp, err := s.HttpClient.Get(s.url("/v1/jobs?stale"))
|
||||
resp, err := s.HttpClient.Get(s.url("/v1/agent/self"))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -226,7 +226,6 @@ func (s *TestServer) waitForLeader() {
|
|||
|
||||
// Ensure we have a leader and a node registeration
|
||||
if leader := resp.Header.Get("X-Nomad-KnownLeader"); leader != "true" {
|
||||
fmt.Println(leader)
|
||||
return false, fmt.Errorf("Nomad leader status: %#v", leader)
|
||||
}
|
||||
return true, nil
|
||||
|
|
Loading…
Reference in a new issue