diff --git a/command/ls/ls.go b/command/ls/ls.go deleted file mode 100644 index 9ec2adc..0000000 --- a/command/ls/ls.go +++ /dev/null @@ -1,28 +0,0 @@ -package ls - -import ( - "fmt" - "os" - - "git.j3s.sh/cascade/command/ls/members" -) - -const usage = `cascade ls members -cascade ls nodes -cascade ls services -` - -func Run(args []string) { - if len(args) == 0 { - fmt.Printf(usage) - os.Exit(1) - } - subcommand := args[0] - switch subcommand { - case "members": - members.Run(args[1:]) - default: - fmt.Fprintf(os.Stderr, "'%s' is not a valid subcommand\n\n%s", subcommand, usage) - os.Exit(1) - } -} diff --git a/command/ls/nodes/nodes.go b/command/ls/nodes/nodes.go deleted file mode 100644 index bf5fae9..0000000 --- a/command/ls/nodes/nodes.go +++ /dev/null @@ -1,14 +0,0 @@ -package nodes - -import ( - "fmt" -) - -const usage = `cascade list|ls nodes -cascade list|ls members -cascade list|ls services -` - -func Run(args []string) { - fmt.Printf("%+v", usage) -} diff --git a/agent/agent.go b/internal/agent/agent.go similarity index 100% rename from agent/agent.go rename to internal/agent/agent.go diff --git a/agent/agent_endpoint.go b/internal/agent/agent_endpoint.go similarity index 100% rename from agent/agent_endpoint.go rename to internal/agent/agent_endpoint.go diff --git a/agent/config.go b/internal/agent/config.go similarity index 100% rename from agent/config.go rename to internal/agent/config.go diff --git a/agent/http.go b/internal/agent/http.go similarity index 100% rename from agent/http.go rename to internal/agent/http.go diff --git a/agent/service.go b/internal/agent/service.go similarity index 100% rename from agent/service.go rename to internal/agent/service.go diff --git a/command/agent/agent.go b/internal/cli/agent.go similarity index 52% rename from command/agent/agent.go rename to internal/cli/agent.go index ae122fe..935d773 100644 --- a/command/agent/agent.go +++ b/internal/cli/agent.go @@ -1,31 +1,39 @@ -package agent +package cli import ( "flag" "fmt" "log" - "net" "os" "os/signal" - "strconv" "strings" "syscall" "time" - "git.j3s.sh/cascade/agent" + "git.j3s.sh/cascade/internal/agent" ) -// gracefulTimeout controls how long we wait before forcefully terminating -// note that this value interacts with serf's LeavePropagateDelay config -const gracefulTimeout = 10 * time.Second +type agentCommand struct { + // gracefulTimeout controls how long we wait before forcefully terminating + // note that this value interacts with serf's LeavePropagateDelay config + gracefulTimeout time.Duration -const usage = `cascade agent [options] + flagBindDNS string + flagBindHTTP string + flagBindSerf string + flagJoin string + flagNode string +} + +func (c *agentCommand) Usage() { + fmt.Print( + `usage: cascade agent [flags] this command starts the cascade agent, which is responsible for basically everything, including service registration, health checking, cluster membership, and hosting the API. -options: +flags: -bind-dns= address the DNS server binds to (default = 127.0.0.1:8600) @@ -40,31 +48,33 @@ options: -node= name of this node, must be globally unique (default = hostname) -` - -type Flags struct { - bindDNS string - bindHTTP string - bindSerf string - join string - node string +`) } -var agentFlags Flags +func (c *agentCommand) Init(args []string) agentCommand { + c.gracefulTimeout = 10 * time.Second + + flags := flag.NewFlagSet("", flag.ContinueOnError) + flags.Usage = c.Usage + flags.StringVar(&c.flagBindDNS, "bind-dns", "", "") + flags.StringVar(&c.flagBindHTTP, "bind-http", "", "") + flags.StringVar(&c.flagBindSerf, "bind-serf", "", "") + flags.StringVar(&c.flagJoin, "join", "", "") + flags.StringVar(&c.flagNode, "node", "", "") -func Run(args []string) { - flags := flag.NewFlagSet("agent", flag.ContinueOnError) - flags.Usage = func() { fmt.Printf(usage) } - flags.StringVar(&agentFlags.bindDNS, "bind-dns", "", "") - flags.StringVar(&agentFlags.bindHTTP, "bind-http", "", "") - flags.StringVar(&agentFlags.bindSerf, "bind-serf", "", "") - flags.StringVar(&agentFlags.join, "join", "", "") - flags.StringVar(&agentFlags.node, "node", "", "") if err := flags.Parse(args); err != nil { + fmt.Println(err) os.Exit(1) } - config, err := getAgentConfig() + return *c +} + +func RunAgent(args []string) { + c := agentCommand{} + c.Init(args) + + config, err := c.getAgentConfig() if err != nil { fmt.Println(err) os.Exit(1) @@ -88,18 +98,18 @@ func Run(args []string) { } defer agent.Shutdown() - if err := startupJoin(agent); err != nil { + if err := c.startupJoin(agent); err != nil { fmt.Println(err) os.Exit(1) } - if err := handleSignals(agent); err != nil { + if err := c.handleSignals(agent); err != nil { fmt.Println(err) os.Exit(1) } } // handleSignals blocks until we get an exit-causing signal -func handleSignals(agent *agent.Agent) error { +func (c agentCommand) handleSignals(agent *agent.Agent) error { signalCh := make(chan os.Signal, 4) signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) @@ -141,14 +151,14 @@ func handleSignals(agent *agent.Agent) error { select { case <-signalCh: return fmt.Errorf("idfk") - case <-time.After(gracefulTimeout): + case <-time.After(c.gracefulTimeout): return fmt.Errorf("leave timed out") case <-gracefulCh: return nil } } -func startupJoin(a *agent.Agent) error { +func (c agentCommand) startupJoin(a *agent.Agent) error { if len(a.Config.StartJoin) == 0 { return nil } @@ -166,67 +176,31 @@ func startupJoin(a *agent.Agent) error { // getAgentConfig takes a default agent config and modifies it based // on user specified flags. It also does some a little input validation. -func getAgentConfig() (*agent.Config, error) { +func (c agentCommand) getAgentConfig() (*agent.Config, error) { config := agent.DefaultConfig() - if agentFlags.bindDNS != "" { - if err := parseFlagAddress(agentFlags.bindDNS, config.DNSBindAddr); err != nil { + if c.flagBindDNS != "" { + if err := parseFlagAddress(c.flagBindDNS, config.DNSBindAddr); err != nil { return nil, err } } - if agentFlags.bindHTTP != "" { - if err := parseFlagAddress(agentFlags.bindHTTP, config.HTTPBindAddr); err != nil { + if c.flagBindHTTP != "" { + if err := parseFlagAddress(c.flagBindHTTP, config.HTTPBindAddr); err != nil { return nil, err } } - if agentFlags.bindSerf != "" { - if err := parseFlagAddress(agentFlags.bindSerf, config.SerfBindAddr); err != nil { + if c.flagBindSerf != "" { + if err := parseFlagAddress(c.flagBindSerf, config.SerfBindAddr); err != nil { return nil, err } } - if agentFlags.join != "" { + if c.flagJoin != "" { // TODO: moar validation - config.StartJoin = strings.Split(agentFlags.join, ",") + config.StartJoin = strings.Split(c.flagJoin, ",") } - if agentFlags.node != "" { - config.NodeName = agentFlags.node + if c.flagNode != "" { + config.NodeName = c.flagNode } return config, nil } - -// parseFlagAddress takes a colon-delimited host:port pair as a string, parses -// out the ip (and optionally, the port), and modifies the passed TCPAddr with -// the resulting values. -func parseFlagAddress(hostPort string, tcpAddr *net.TCPAddr) error { - addr, portStr, err := net.SplitHostPort(hostPort) - if err != nil { - if !strings.Contains(err.Error(), "missing port in address") { - return fmt.Errorf("Error parsing address: %v", err) - } - - // If we get a missing port error, we try to coerce the whole hostPort - // into an address. This allows the user to supply just a host address - // instead of always requiring a host:ip pair. - addr = hostPort - } - - if addr == "" { - return fmt.Errorf("Error parsing blank address") - } - ip := net.ParseIP(addr) - if ip == nil { - return fmt.Errorf("Error parsing address %q: not a valid IP address", ip) - } - - if portStr != "" { - port, err := strconv.Atoi(portStr) - if err != nil { - return fmt.Errorf("Error parsing port: %s", err) - } - tcpAddr.Port = port - } - tcpAddr.IP = ip - - return nil -} diff --git a/command/agent/agent_test.go b/internal/cli/agent_test.go similarity index 97% rename from command/agent/agent_test.go rename to internal/cli/agent_test.go index fa279bc..f213f74 100644 --- a/command/agent/agent_test.go +++ b/internal/cli/agent_test.go @@ -1,10 +1,10 @@ -package agent +package cli import ( "strings" "testing" - "git.j3s.sh/cascade/agent" + "git.j3s.sh/cascade/internal/agent" ) func TestParseFlagAddress(t *testing.T) { diff --git a/command/ls/members/members.go b/internal/cli/members.go similarity index 53% rename from command/ls/members/members.go rename to internal/cli/members.go index c193d7d..99a851b 100644 --- a/command/ls/members/members.go +++ b/internal/cli/members.go @@ -1,4 +1,4 @@ -package members +package cli import ( "bytes" @@ -11,34 +11,41 @@ import ( "git.j3s.sh/cascade/api" ) -const usage = `cascade ls members [options] +type membersCommand struct { + usage string - this command lists the members of the cascade serf - cluster. - -options: + flagAPIAddr string + flagDetails bool +} +func (c membersCommand) Usage() { + fmt.Printf(`usage: cascade members [flags] + list the members of the cascade serf cluster + +flags: -api - address of the api to target (default = 127.0.0.1:8500) - - -l | -details + address of the cascade http api to target (default = 127.0.0.1:8500) + + -details | -l show more details (default = nil) -` - -type Flags struct { - apiAddr string - details bool +`) } -var membersFlags Flags +func (c *membersCommand) Init(args []string) { + flags := flag.NewFlagSet("", flag.ContinueOnError) + flags.Usage = c.Usage + flags.StringVar(&c.flagAPIAddr, "api", "", "") + flags.BoolVar(&c.flagDetails, "details", false, "") + flags.BoolVar(&c.flagDetails, "l", false, "") + if err := flags.Parse(args); err != nil { + fmt.Println(err) + os.Exit(1) + } +} -func Run(args []string) { - flags := flag.NewFlagSet("agent", flag.ContinueOnError) - flags.Usage = func() { fmt.Printf(usage) } - flags.StringVar(&membersFlags.apiAddr, "api", "", "") - flags.BoolVar(&membersFlags.details, "l", false, "") - flags.BoolVar(&membersFlags.details, "details", false, "") - flags.Parse(args) +func RunMembers(args []string) { + c := membersCommand{} + c.Init(args) cfg := api.DefaultConfig() client, err := api.NewClient(cfg) @@ -52,7 +59,7 @@ func Run(args []string) { os.Exit(1) } - if membersFlags.details { + if c.flagDetails { var b bytes.Buffer tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) fmt.Fprintf(tw, "node\taddr\tstatus\ttags\n") @@ -60,7 +67,7 @@ func Run(args []string) { fmt.Fprintf(tw, "%s\t", m.Name) fmt.Fprintf(tw, "%s:%d\t", m.Addr, m.Port) fmt.Fprintf(tw, "%s\t", m.StatusPretty()) - fmt.Fprintf(tw, "%s\t", printTags(m.Tags)) + fmt.Fprintf(tw, "%s\t", c.printTags(m.Tags)) fmt.Fprintln(tw) } if err := tw.Flush(); err != nil { @@ -75,7 +82,7 @@ func Run(args []string) { } } -func printTags(tags map[string]string) string { +func (c membersCommand) printTags(tags map[string]string) string { var results []string for k, v := range tags { results = append(results, fmt.Sprintf("%s=%s", k, v)) diff --git a/command/ls/services/services.go b/internal/cli/services.go similarity index 90% rename from command/ls/services/services.go rename to internal/cli/services.go index 0a83c90..0457364 100644 --- a/command/ls/services/services.go +++ b/internal/cli/services.go @@ -1,4 +1,4 @@ -package services +package cli import ( "fmt" diff --git a/internal/cli/utils.go b/internal/cli/utils.go new file mode 100644 index 0000000..2fb360e --- /dev/null +++ b/internal/cli/utils.go @@ -0,0 +1,44 @@ +package cli + +import ( + "fmt" + "net" + "strconv" + "strings" +) + +// parseFlagAddress takes a colon-delimited host:port pair as a string, parses +// out the ip (and optionally, the port), and modifies the passed TCPAddr with +// the resulting values. +func parseFlagAddress(hostPort string, tcpAddr *net.TCPAddr) error { + addr, portStr, err := net.SplitHostPort(hostPort) + if err != nil { + if !strings.Contains(err.Error(), "missing port in address") { + return fmt.Errorf("Error parsing address: %v", err) + } + + // If we get a missing port error, we try to coerce the whole hostPort + // into an address. This allows the user to supply just a host address + // instead of always requiring a host:ip pair. + addr = hostPort + } + + if addr == "" { + return fmt.Errorf("Error parsing blank address") + } + ip := net.ParseIP(addr) + if ip == nil { + return fmt.Errorf("Error parsing address %q: not a valid IP address", ip) + } + + if portStr != "" { + port, err := strconv.Atoi(portStr) + if err != nil { + return fmt.Errorf("Error parsing port: %s", err) + } + tcpAddr.Port = port + } + tcpAddr.IP = ip + + return nil +} diff --git a/main.go b/main.go index b9d34e3..a5cb85d 100644 --- a/main.go +++ b/main.go @@ -4,13 +4,16 @@ import ( "fmt" "os" - "git.j3s.sh/cascade/command/agent" - "git.j3s.sh/cascade/command/ls" + "git.j3s.sh/cascade/internal/cli" ) -const usage = `cascade agent start a cascade agent -cascade ls list nodes, services, or members -cascade rtt estimate latency between nodes +const usage = ` usage: cascade [command] [flags] + + commands: + cascade agent start a cascade agent + cascade members list cluster members + cascade status overview of the cascade cluster + cascade rtt estimate latency between nodes ` // TODO: rename agent to something cooler @@ -24,9 +27,9 @@ func main() { args := os.Args[2:] switch os.Args[1] { case "agent": - agent.Run(args) - case "ls": - ls.Run(args) + cli.RunAgent(args) + case "members": + cli.RunMembers(args) default: fmt.Fprintf(os.Stderr, "'%s' is not a valid command\n\n%s", command, usage) os.Exit(1)