···11+package cli
22+33+import (
44+ "flag"
55+ "fmt"
66+ "log"
77+ "os"
88+ "os/signal"
99+ "strings"
1010+ "syscall"
1111+ "time"
1212+1313+ "git.j3s.sh/cascade/internal/agent"
1414+)
1515+1616+type agentCommand struct {
1717+ // gracefulTimeout controls how long we wait before forcefully terminating
1818+ // note that this value interacts with serf's LeavePropagateDelay config
1919+ gracefulTimeout time.Duration
2020+2121+ flagBindDNS string
2222+ flagBindHTTP string
2323+ flagBindSerf string
2424+ flagJoin string
2525+ flagNode string
2626+}
2727+2828+func (c *agentCommand) Usage() {
2929+ fmt.Print(
3030+ `usage: cascade agent [flags]
3131+3232+ this command starts the cascade agent, which is responsible
3333+ for basically everything, including service registration,
3434+ health checking, cluster membership, and hosting the API.
3535+3636+flags:
3737+ -bind-dns=<addr>
3838+ address the DNS server binds to (default = 127.0.0.1:8600)
3939+4040+ -bind-http=<addr>
4141+ address the http api/interace binds to (default = 127.0.0.1:8500)
4242+4343+ -bind-serf=<addr>
4444+ address the serf agent binds to (default = 0.0.0.0:8301)
4545+4646+ -join=<addrs>
4747+ comma-separated address of agents to join at start time (default = nil)
4848+4949+ -node=<name>
5050+ name of this node, must be globally unique (default = hostname)
5151+`)
5252+}
5353+5454+func (c *agentCommand) Init(args []string) agentCommand {
5555+ c.gracefulTimeout = 10 * time.Second
5656+5757+ flags := flag.NewFlagSet("", flag.ContinueOnError)
5858+ flags.Usage = c.Usage
5959+ flags.StringVar(&c.flagBindDNS, "bind-dns", "", "")
6060+ flags.StringVar(&c.flagBindHTTP, "bind-http", "", "")
6161+ flags.StringVar(&c.flagBindSerf, "bind-serf", "", "")
6262+ flags.StringVar(&c.flagJoin, "join", "", "")
6363+ flags.StringVar(&c.flagNode, "node", "", "")
6464+6565+ if err := flags.Parse(args); err != nil {
6666+ fmt.Println(err)
6767+ os.Exit(1)
6868+ }
6969+7070+ return *c
7171+}
7272+7373+func RunAgent(args []string) {
7474+ c := agentCommand{}
7575+ c.Init(args)
7676+7777+ config, err := c.getAgentConfig()
7878+ if err != nil {
7979+ fmt.Println(err)
8080+ os.Exit(1)
8181+ }
8282+ agent := agent.New(config)
8383+8484+ fmt.Printf("-> starting cascade agent\n")
8585+ fmt.Printf(" node '%s'\n", config.NodeName)
8686+ if len(config.StartJoin) > 0 {
8787+ fmt.Printf(" join '%s'\n", config.StartJoin)
8888+ }
8989+ fmt.Printf(" bind addrs:\n")
9090+ fmt.Printf(" dns '%s'\n", config.DNSBindAddr)
9191+ fmt.Printf(" http '%s'\n", config.HTTPBindAddr)
9292+ fmt.Printf(" serf '%s'\n", config.SerfBindAddr)
9393+ fmt.Printf("\n-> logs\n")
9494+9595+ if err := agent.Start(); err != nil {
9696+ fmt.Println(err)
9797+ os.Exit(1)
9898+ }
9999+ defer agent.Shutdown()
100100+101101+ if err := c.startupJoin(agent); err != nil {
102102+ fmt.Println(err)
103103+ os.Exit(1)
104104+ }
105105+ if err := c.handleSignals(agent); err != nil {
106106+ fmt.Println(err)
107107+ os.Exit(1)
108108+ }
109109+}
110110+111111+// handleSignals blocks until we get an exit-causing signal
112112+func (c agentCommand) handleSignals(agent *agent.Agent) error {
113113+ signalCh := make(chan os.Signal, 4)
114114+ signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
115115+116116+ // Wait for a signal
117117+ var sig os.Signal
118118+ select {
119119+ case s := <-signalCh:
120120+ sig = s
121121+ case <-agent.ShutdownCh():
122122+ // Agent is already shutdown!
123123+ return nil
124124+ }
125125+ fmt.Fprintf(os.Stderr, "caught signal %s", sig)
126126+127127+ // Check if we should do a graceful leave
128128+ graceful := false
129129+ if sig == os.Interrupt || sig == syscall.SIGTERM {
130130+ graceful = true
131131+ }
132132+133133+ // Bail fast if not doing a graceful leave
134134+ if !graceful {
135135+ fmt.Fprintf(os.Stderr, "leave cluster with zero grace")
136136+ return nil
137137+ }
138138+139139+ // Attempt a graceful leave
140140+ gracefulCh := make(chan struct{})
141141+ fmt.Printf("shutting down gracefully")
142142+ go func() {
143143+ if err := agent.Leave(); err != nil {
144144+ fmt.Println("Error: ", err)
145145+ return
146146+ }
147147+ close(gracefulCh)
148148+ }()
149149+150150+ // Wait for leave or another signal
151151+ select {
152152+ case <-signalCh:
153153+ return fmt.Errorf("idfk")
154154+ case <-time.After(c.gracefulTimeout):
155155+ return fmt.Errorf("leave timed out")
156156+ case <-gracefulCh:
157157+ return nil
158158+ }
159159+}
160160+161161+func (c agentCommand) startupJoin(a *agent.Agent) error {
162162+ if len(a.Config.StartJoin) == 0 {
163163+ return nil
164164+ }
165165+166166+ n, err := a.Join(a.Config.StartJoin)
167167+ if err != nil {
168168+ return err
169169+ }
170170+ if n > 0 {
171171+ log.Printf("issue join request nodes=%d\n", n)
172172+ }
173173+174174+ return nil
175175+}
176176+177177+// getAgentConfig takes a default agent config and modifies it based
178178+// on user specified flags. It also does some a little input validation.
179179+func (c agentCommand) getAgentConfig() (*agent.Config, error) {
180180+ config := agent.DefaultConfig()
181181+182182+ if c.flagBindDNS != "" {
183183+ if err := parseFlagAddress(c.flagBindDNS, config.DNSBindAddr); err != nil {
184184+ return nil, err
185185+ }
186186+ }
187187+ if c.flagBindHTTP != "" {
188188+ if err := parseFlagAddress(c.flagBindHTTP, config.HTTPBindAddr); err != nil {
189189+ return nil, err
190190+ }
191191+ }
192192+ if c.flagBindSerf != "" {
193193+ if err := parseFlagAddress(c.flagBindSerf, config.SerfBindAddr); err != nil {
194194+ return nil, err
195195+ }
196196+ }
197197+ if c.flagJoin != "" {
198198+ // TODO: moar validation
199199+ config.StartJoin = strings.Split(c.flagJoin, ",")
200200+ }
201201+202202+ if c.flagNode != "" {
203203+ config.NodeName = c.flagNode
204204+ }
205205+ return config, nil
206206+}
+91
internal/cli/members.go
···11+package cli
22+33+import (
44+ "bytes"
55+ "flag"
66+ "fmt"
77+ "os"
88+ "strings"
99+ "text/tabwriter"
1010+1111+ "git.j3s.sh/cascade/api"
1212+)
1313+1414+type membersCommand struct {
1515+ usage string
1616+1717+ flagAPIAddr string
1818+ flagDetails bool
1919+}
2020+2121+func (c membersCommand) Usage() {
2222+ fmt.Printf(`usage: cascade members [flags]
2323+ list the members of the cascade serf cluster
2424+2525+flags:
2626+ -api
2727+ address of the cascade http api to target (default = 127.0.0.1:8500)
2828+2929+ -details | -l
3030+ show more details (default = nil)
3131+`)
3232+}
3333+3434+func (c *membersCommand) Init(args []string) {
3535+ flags := flag.NewFlagSet("", flag.ContinueOnError)
3636+ flags.Usage = c.Usage
3737+ flags.StringVar(&c.flagAPIAddr, "api", "", "")
3838+ flags.BoolVar(&c.flagDetails, "details", false, "")
3939+ flags.BoolVar(&c.flagDetails, "l", false, "")
4040+ if err := flags.Parse(args); err != nil {
4141+ fmt.Println(err)
4242+ os.Exit(1)
4343+ }
4444+}
4545+4646+func RunMembers(args []string) {
4747+ c := membersCommand{}
4848+ c.Init(args)
4949+5050+ cfg := api.DefaultConfig()
5151+ client, err := api.NewClient(cfg)
5252+ if err != nil {
5353+ fmt.Println(err)
5454+ os.Exit(1)
5555+ }
5656+ members, err := client.Agent().Members()
5757+ if err != nil {
5858+ fmt.Println(err)
5959+ os.Exit(1)
6060+ }
6161+6262+ if c.flagDetails {
6363+ var b bytes.Buffer
6464+ tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
6565+ fmt.Fprintf(tw, "node\taddr\tstatus\ttags\n")
6666+ for _, m := range members {
6767+ fmt.Fprintf(tw, "%s\t", m.Name)
6868+ fmt.Fprintf(tw, "%s:%d\t", m.Addr, m.Port)
6969+ fmt.Fprintf(tw, "%s\t", m.StatusPretty())
7070+ fmt.Fprintf(tw, "%s\t", c.printTags(m.Tags))
7171+ fmt.Fprintln(tw)
7272+ }
7373+ if err := tw.Flush(); err != nil {
7474+ fmt.Printf("error flushing tabwriter: %s", err)
7575+ os.Exit(1)
7676+ }
7777+ fmt.Print(b.String())
7878+ } else {
7979+ for _, m := range members {
8080+ fmt.Println(m.Name)
8181+ }
8282+ }
8383+}
8484+8585+func (c membersCommand) printTags(tags map[string]string) string {
8686+ var results []string
8787+ for k, v := range tags {
8888+ results = append(results, fmt.Sprintf("%s=%s", k, v))
8989+ }
9090+ return strings.Join(results, ",")
9191+}
+44
internal/cli/utils.go
···11+package cli
22+33+import (
44+ "fmt"
55+ "net"
66+ "strconv"
77+ "strings"
88+)
99+1010+// parseFlagAddress takes a colon-delimited host:port pair as a string, parses
1111+// out the ip (and optionally, the port), and modifies the passed TCPAddr with
1212+// the resulting values.
1313+func parseFlagAddress(hostPort string, tcpAddr *net.TCPAddr) error {
1414+ addr, portStr, err := net.SplitHostPort(hostPort)
1515+ if err != nil {
1616+ if !strings.Contains(err.Error(), "missing port in address") {
1717+ return fmt.Errorf("Error parsing address: %v", err)
1818+ }
1919+2020+ // If we get a missing port error, we try to coerce the whole hostPort
2121+ // into an address. This allows the user to supply just a host address
2222+ // instead of always requiring a host:ip pair.
2323+ addr = hostPort
2424+ }
2525+2626+ if addr == "" {
2727+ return fmt.Errorf("Error parsing blank address")
2828+ }
2929+ ip := net.ParseIP(addr)
3030+ if ip == nil {
3131+ return fmt.Errorf("Error parsing address %q: not a valid IP address", ip)
3232+ }
3333+3434+ if portStr != "" {
3535+ port, err := strconv.Atoi(portStr)
3636+ if err != nil {
3737+ return fmt.Errorf("Error parsing port: %s", err)
3838+ }
3939+ tcpAddr.Port = port
4040+ }
4141+ tcpAddr.IP = ip
4242+4343+ return nil
4444+}
+11-8
main.go
···44 "fmt"
55 "os"
6677- "git.j3s.sh/cascade/command/agent"
88- "git.j3s.sh/cascade/command/ls"
77+ "git.j3s.sh/cascade/internal/cli"
98)
1091111-const usage = `cascade agent start a cascade agent
1212-cascade ls list nodes, services, or members
1313-cascade rtt estimate latency between nodes
1010+const usage = ` usage: cascade [command] [flags]
1111+1212+ commands:
1313+ cascade agent start a cascade agent
1414+ cascade members list cluster members
1515+ cascade status overview of the cascade cluster
1616+ cascade rtt estimate latency between nodes
1417`
15181619// TODO: rename agent to something cooler
···2427 args := os.Args[2:]
2528 switch os.Args[1] {
2629 case "agent":
2727- agent.Run(args)
2828- case "ls":
2929- ls.Run(args)
3030+ cli.RunAgent(args)
3131+ case "members":
3232+ cli.RunMembers(args)
3033 default:
3134 fmt.Fprintf(os.Stderr, "'%s' is not a valid command\n\n%s", command, usage)
3235 os.Exit(1)