leaderless gossip-based service discovery w/ consul compatibility
5

Configure Feed

Select the types of activity you want to include in your feed.

reorganize everything for my sanity

Jes Olson (Mar 6, 2023, 9:57 PM -0800) 9adc0c6c 485812f5

+355 -369
agent/agent.go internal/agent/agent.go
agent/agent_endpoint.go internal/agent/agent_endpoint.go
agent/config.go internal/agent/config.go
agent/http.go internal/agent/http.go
agent/service.go internal/agent/service.go
-232
command/agent/agent.go
··· 1 - package agent 2 - 3 - import ( 4 - "flag" 5 - "fmt" 6 - "log" 7 - "net" 8 - "os" 9 - "os/signal" 10 - "strconv" 11 - "strings" 12 - "syscall" 13 - "time" 14 - 15 - "git.j3s.sh/cascade/agent" 16 - ) 17 - 18 - // gracefulTimeout controls how long we wait before forcefully terminating 19 - // note that this value interacts with serf's LeavePropagateDelay config 20 - const gracefulTimeout = 10 * time.Second 21 - 22 - const usage = `cascade agent [options] 23 - 24 - this command starts the cascade agent, which is responsible 25 - for basically everything, including service registration, 26 - health checking, cluster membership, and hosting the API. 27 - 28 - options: 29 - -bind-dns=<addr> 30 - address the DNS server binds to (default = 127.0.0.1:8600) 31 - 32 - -bind-http=<addr> 33 - address the http api/interace binds to (default = 127.0.0.1:8500) 34 - 35 - -bind-serf=<addr> 36 - address the serf agent binds to (default = 0.0.0.0:8301) 37 - 38 - -join=<addrs> 39 - comma-separated address of agents to join at start time (default = nil) 40 - 41 - -node=<name> 42 - name of this node, must be globally unique (default = hostname) 43 - ` 44 - 45 - type Flags struct { 46 - bindDNS string 47 - bindHTTP string 48 - bindSerf string 49 - join string 50 - node string 51 - } 52 - 53 - var agentFlags Flags 54 - 55 - func Run(args []string) { 56 - flags := flag.NewFlagSet("agent", flag.ContinueOnError) 57 - flags.Usage = func() { fmt.Printf(usage) } 58 - flags.StringVar(&agentFlags.bindDNS, "bind-dns", "", "") 59 - flags.StringVar(&agentFlags.bindHTTP, "bind-http", "", "") 60 - flags.StringVar(&agentFlags.bindSerf, "bind-serf", "", "") 61 - flags.StringVar(&agentFlags.join, "join", "", "") 62 - flags.StringVar(&agentFlags.node, "node", "", "") 63 - if err := flags.Parse(args); err != nil { 64 - os.Exit(1) 65 - } 66 - 67 - config, err := getAgentConfig() 68 - if err != nil { 69 - fmt.Println(err) 70 - os.Exit(1) 71 - } 72 - agent := agent.New(config) 73 - 74 - fmt.Printf("-> starting cascade agent\n") 75 - fmt.Printf(" node '%s'\n", config.NodeName) 76 - if len(config.StartJoin) > 0 { 77 - fmt.Printf(" join '%s'\n", config.StartJoin) 78 - } 79 - fmt.Printf(" bind addrs:\n") 80 - fmt.Printf(" dns '%s'\n", config.DNSBindAddr) 81 - fmt.Printf(" http '%s'\n", config.HTTPBindAddr) 82 - fmt.Printf(" serf '%s'\n", config.SerfBindAddr) 83 - fmt.Printf("\n-> logs\n") 84 - 85 - if err := agent.Start(); err != nil { 86 - fmt.Println(err) 87 - os.Exit(1) 88 - } 89 - defer agent.Shutdown() 90 - 91 - if err := startupJoin(agent); err != nil { 92 - fmt.Println(err) 93 - os.Exit(1) 94 - } 95 - if err := handleSignals(agent); err != nil { 96 - fmt.Println(err) 97 - os.Exit(1) 98 - } 99 - } 100 - 101 - // handleSignals blocks until we get an exit-causing signal 102 - func handleSignals(agent *agent.Agent) error { 103 - signalCh := make(chan os.Signal, 4) 104 - signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) 105 - 106 - // Wait for a signal 107 - var sig os.Signal 108 - select { 109 - case s := <-signalCh: 110 - sig = s 111 - case <-agent.ShutdownCh(): 112 - // Agent is already shutdown! 113 - return nil 114 - } 115 - fmt.Fprintf(os.Stderr, "caught signal %s", sig) 116 - 117 - // Check if we should do a graceful leave 118 - graceful := false 119 - if sig == os.Interrupt || sig == syscall.SIGTERM { 120 - graceful = true 121 - } 122 - 123 - // Bail fast if not doing a graceful leave 124 - if !graceful { 125 - fmt.Fprintf(os.Stderr, "leave cluster with zero grace") 126 - return nil 127 - } 128 - 129 - // Attempt a graceful leave 130 - gracefulCh := make(chan struct{}) 131 - fmt.Printf("shutting down gracefully") 132 - go func() { 133 - if err := agent.Leave(); err != nil { 134 - fmt.Println("Error: ", err) 135 - return 136 - } 137 - close(gracefulCh) 138 - }() 139 - 140 - // Wait for leave or another signal 141 - select { 142 - case <-signalCh: 143 - return fmt.Errorf("idfk") 144 - case <-time.After(gracefulTimeout): 145 - return fmt.Errorf("leave timed out") 146 - case <-gracefulCh: 147 - return nil 148 - } 149 - } 150 - 151 - func startupJoin(a *agent.Agent) error { 152 - if len(a.Config.StartJoin) == 0 { 153 - return nil 154 - } 155 - 156 - n, err := a.Join(a.Config.StartJoin) 157 - if err != nil { 158 - return err 159 - } 160 - if n > 0 { 161 - log.Printf("issue join request nodes=%d\n", n) 162 - } 163 - 164 - return nil 165 - } 166 - 167 - // getAgentConfig takes a default agent config and modifies it based 168 - // on user specified flags. It also does some a little input validation. 169 - func getAgentConfig() (*agent.Config, error) { 170 - config := agent.DefaultConfig() 171 - 172 - if agentFlags.bindDNS != "" { 173 - if err := parseFlagAddress(agentFlags.bindDNS, config.DNSBindAddr); err != nil { 174 - return nil, err 175 - } 176 - } 177 - if agentFlags.bindHTTP != "" { 178 - if err := parseFlagAddress(agentFlags.bindHTTP, config.HTTPBindAddr); err != nil { 179 - return nil, err 180 - } 181 - } 182 - if agentFlags.bindSerf != "" { 183 - if err := parseFlagAddress(agentFlags.bindSerf, config.SerfBindAddr); err != nil { 184 - return nil, err 185 - } 186 - } 187 - if agentFlags.join != "" { 188 - // TODO: moar validation 189 - config.StartJoin = strings.Split(agentFlags.join, ",") 190 - } 191 - 192 - if agentFlags.node != "" { 193 - config.NodeName = agentFlags.node 194 - } 195 - return config, nil 196 - } 197 - 198 - // parseFlagAddress takes a colon-delimited host:port pair as a string, parses 199 - // out the ip (and optionally, the port), and modifies the passed TCPAddr with 200 - // the resulting values. 201 - func parseFlagAddress(hostPort string, tcpAddr *net.TCPAddr) error { 202 - addr, portStr, err := net.SplitHostPort(hostPort) 203 - if err != nil { 204 - if !strings.Contains(err.Error(), "missing port in address") { 205 - return fmt.Errorf("Error parsing address: %v", err) 206 - } 207 - 208 - // If we get a missing port error, we try to coerce the whole hostPort 209 - // into an address. This allows the user to supply just a host address 210 - // instead of always requiring a host:ip pair. 211 - addr = hostPort 212 - } 213 - 214 - if addr == "" { 215 - return fmt.Errorf("Error parsing blank address") 216 - } 217 - ip := net.ParseIP(addr) 218 - if ip == nil { 219 - return fmt.Errorf("Error parsing address %q: not a valid IP address", ip) 220 - } 221 - 222 - if portStr != "" { 223 - port, err := strconv.Atoi(portStr) 224 - if err != nil { 225 - return fmt.Errorf("Error parsing port: %s", err) 226 - } 227 - tcpAddr.Port = port 228 - } 229 - tcpAddr.IP = ip 230 - 231 - return nil 232 - }
+2 -2
command/agent/agent_test.go internal/cli/agent_test.go
··· 1 - package agent 1 + package cli 2 2 3 3 import ( 4 4 "strings" 5 5 "testing" 6 6 7 - "git.j3s.sh/cascade/agent" 7 + "git.j3s.sh/cascade/internal/agent" 8 8 ) 9 9 10 10 func TestParseFlagAddress(t *testing.T) {
-28
command/ls/ls.go
··· 1 - package ls 2 - 3 - import ( 4 - "fmt" 5 - "os" 6 - 7 - "git.j3s.sh/cascade/command/ls/members" 8 - ) 9 - 10 - const usage = `cascade ls members 11 - cascade ls nodes 12 - cascade ls services 13 - ` 14 - 15 - func Run(args []string) { 16 - if len(args) == 0 { 17 - fmt.Printf(usage) 18 - os.Exit(1) 19 - } 20 - subcommand := args[0] 21 - switch subcommand { 22 - case "members": 23 - members.Run(args[1:]) 24 - default: 25 - fmt.Fprintf(os.Stderr, "'%s' is not a valid subcommand\n\n%s", subcommand, usage) 26 - os.Exit(1) 27 - } 28 - }
-84
command/ls/members/members.go
··· 1 - package members 2 - 3 - import ( 4 - "bytes" 5 - "flag" 6 - "fmt" 7 - "os" 8 - "strings" 9 - "text/tabwriter" 10 - 11 - "git.j3s.sh/cascade/api" 12 - ) 13 - 14 - const usage = `cascade ls members [options] 15 - 16 - this command lists the members of the cascade serf 17 - cluster. 18 - 19 - options: 20 - 21 - -api 22 - address of the api to target (default = 127.0.0.1:8500) 23 - 24 - -l | -details 25 - show more details (default = nil) 26 - ` 27 - 28 - type Flags struct { 29 - apiAddr string 30 - details bool 31 - } 32 - 33 - var membersFlags Flags 34 - 35 - func Run(args []string) { 36 - flags := flag.NewFlagSet("agent", flag.ContinueOnError) 37 - flags.Usage = func() { fmt.Printf(usage) } 38 - flags.StringVar(&membersFlags.apiAddr, "api", "", "") 39 - flags.BoolVar(&membersFlags.details, "l", false, "") 40 - flags.BoolVar(&membersFlags.details, "details", false, "") 41 - flags.Parse(args) 42 - 43 - cfg := api.DefaultConfig() 44 - client, err := api.NewClient(cfg) 45 - if err != nil { 46 - fmt.Println(err) 47 - os.Exit(1) 48 - } 49 - members, err := client.Agent().Members() 50 - if err != nil { 51 - fmt.Println(err) 52 - os.Exit(1) 53 - } 54 - 55 - if membersFlags.details { 56 - var b bytes.Buffer 57 - tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) 58 - fmt.Fprintf(tw, "node\taddr\tstatus\ttags\n") 59 - for _, m := range members { 60 - fmt.Fprintf(tw, "%s\t", m.Name) 61 - fmt.Fprintf(tw, "%s:%d\t", m.Addr, m.Port) 62 - fmt.Fprintf(tw, "%s\t", m.StatusPretty()) 63 - fmt.Fprintf(tw, "%s\t", printTags(m.Tags)) 64 - fmt.Fprintln(tw) 65 - } 66 - if err := tw.Flush(); err != nil { 67 - fmt.Printf("error flushing tabwriter: %s", err) 68 - os.Exit(1) 69 - } 70 - fmt.Print(b.String()) 71 - } else { 72 - for _, m := range members { 73 - fmt.Println(m.Name) 74 - } 75 - } 76 - } 77 - 78 - func printTags(tags map[string]string) string { 79 - var results []string 80 - for k, v := range tags { 81 - results = append(results, fmt.Sprintf("%s=%s", k, v)) 82 - } 83 - return strings.Join(results, ",") 84 - }
+1 -1
command/ls/nodes/nodes.go internal/cli/services.go
··· 1 - package nodes 1 + package cli 2 2 3 3 import ( 4 4 "fmt"
-14
command/ls/services/services.go
··· 1 - package services 2 - 3 - import ( 4 - "fmt" 5 - ) 6 - 7 - const usage = `cascade list|ls nodes 8 - cascade list|ls members 9 - cascade list|ls services 10 - ` 11 - 12 - func Run(args []string) { 13 - fmt.Printf("%+v", usage) 14 - }
+206
internal/cli/agent.go
··· 1 + package cli 2 + 3 + import ( 4 + "flag" 5 + "fmt" 6 + "log" 7 + "os" 8 + "os/signal" 9 + "strings" 10 + "syscall" 11 + "time" 12 + 13 + "git.j3s.sh/cascade/internal/agent" 14 + ) 15 + 16 + type agentCommand struct { 17 + // gracefulTimeout controls how long we wait before forcefully terminating 18 + // note that this value interacts with serf's LeavePropagateDelay config 19 + gracefulTimeout time.Duration 20 + 21 + flagBindDNS string 22 + flagBindHTTP string 23 + flagBindSerf string 24 + flagJoin string 25 + flagNode string 26 + } 27 + 28 + func (c *agentCommand) Usage() { 29 + fmt.Print( 30 + `usage: cascade agent [flags] 31 + 32 + this command starts the cascade agent, which is responsible 33 + for basically everything, including service registration, 34 + health checking, cluster membership, and hosting the API. 35 + 36 + flags: 37 + -bind-dns=<addr> 38 + address the DNS server binds to (default = 127.0.0.1:8600) 39 + 40 + -bind-http=<addr> 41 + address the http api/interace binds to (default = 127.0.0.1:8500) 42 + 43 + -bind-serf=<addr> 44 + address the serf agent binds to (default = 0.0.0.0:8301) 45 + 46 + -join=<addrs> 47 + comma-separated address of agents to join at start time (default = nil) 48 + 49 + -node=<name> 50 + name of this node, must be globally unique (default = hostname) 51 + `) 52 + } 53 + 54 + func (c *agentCommand) Init(args []string) agentCommand { 55 + c.gracefulTimeout = 10 * time.Second 56 + 57 + flags := flag.NewFlagSet("", flag.ContinueOnError) 58 + flags.Usage = c.Usage 59 + flags.StringVar(&c.flagBindDNS, "bind-dns", "", "") 60 + flags.StringVar(&c.flagBindHTTP, "bind-http", "", "") 61 + flags.StringVar(&c.flagBindSerf, "bind-serf", "", "") 62 + flags.StringVar(&c.flagJoin, "join", "", "") 63 + flags.StringVar(&c.flagNode, "node", "", "") 64 + 65 + if err := flags.Parse(args); err != nil { 66 + fmt.Println(err) 67 + os.Exit(1) 68 + } 69 + 70 + return *c 71 + } 72 + 73 + func RunAgent(args []string) { 74 + c := agentCommand{} 75 + c.Init(args) 76 + 77 + config, err := c.getAgentConfig() 78 + if err != nil { 79 + fmt.Println(err) 80 + os.Exit(1) 81 + } 82 + agent := agent.New(config) 83 + 84 + fmt.Printf("-> starting cascade agent\n") 85 + fmt.Printf(" node '%s'\n", config.NodeName) 86 + if len(config.StartJoin) > 0 { 87 + fmt.Printf(" join '%s'\n", config.StartJoin) 88 + } 89 + fmt.Printf(" bind addrs:\n") 90 + fmt.Printf(" dns '%s'\n", config.DNSBindAddr) 91 + fmt.Printf(" http '%s'\n", config.HTTPBindAddr) 92 + fmt.Printf(" serf '%s'\n", config.SerfBindAddr) 93 + fmt.Printf("\n-> logs\n") 94 + 95 + if err := agent.Start(); err != nil { 96 + fmt.Println(err) 97 + os.Exit(1) 98 + } 99 + defer agent.Shutdown() 100 + 101 + if err := c.startupJoin(agent); err != nil { 102 + fmt.Println(err) 103 + os.Exit(1) 104 + } 105 + if err := c.handleSignals(agent); err != nil { 106 + fmt.Println(err) 107 + os.Exit(1) 108 + } 109 + } 110 + 111 + // handleSignals blocks until we get an exit-causing signal 112 + func (c agentCommand) handleSignals(agent *agent.Agent) error { 113 + signalCh := make(chan os.Signal, 4) 114 + signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) 115 + 116 + // Wait for a signal 117 + var sig os.Signal 118 + select { 119 + case s := <-signalCh: 120 + sig = s 121 + case <-agent.ShutdownCh(): 122 + // Agent is already shutdown! 123 + return nil 124 + } 125 + fmt.Fprintf(os.Stderr, "caught signal %s", sig) 126 + 127 + // Check if we should do a graceful leave 128 + graceful := false 129 + if sig == os.Interrupt || sig == syscall.SIGTERM { 130 + graceful = true 131 + } 132 + 133 + // Bail fast if not doing a graceful leave 134 + if !graceful { 135 + fmt.Fprintf(os.Stderr, "leave cluster with zero grace") 136 + return nil 137 + } 138 + 139 + // Attempt a graceful leave 140 + gracefulCh := make(chan struct{}) 141 + fmt.Printf("shutting down gracefully") 142 + go func() { 143 + if err := agent.Leave(); err != nil { 144 + fmt.Println("Error: ", err) 145 + return 146 + } 147 + close(gracefulCh) 148 + }() 149 + 150 + // Wait for leave or another signal 151 + select { 152 + case <-signalCh: 153 + return fmt.Errorf("idfk") 154 + case <-time.After(c.gracefulTimeout): 155 + return fmt.Errorf("leave timed out") 156 + case <-gracefulCh: 157 + return nil 158 + } 159 + } 160 + 161 + func (c agentCommand) startupJoin(a *agent.Agent) error { 162 + if len(a.Config.StartJoin) == 0 { 163 + return nil 164 + } 165 + 166 + n, err := a.Join(a.Config.StartJoin) 167 + if err != nil { 168 + return err 169 + } 170 + if n > 0 { 171 + log.Printf("issue join request nodes=%d\n", n) 172 + } 173 + 174 + return nil 175 + } 176 + 177 + // getAgentConfig takes a default agent config and modifies it based 178 + // on user specified flags. It also does some a little input validation. 179 + func (c agentCommand) getAgentConfig() (*agent.Config, error) { 180 + config := agent.DefaultConfig() 181 + 182 + if c.flagBindDNS != "" { 183 + if err := parseFlagAddress(c.flagBindDNS, config.DNSBindAddr); err != nil { 184 + return nil, err 185 + } 186 + } 187 + if c.flagBindHTTP != "" { 188 + if err := parseFlagAddress(c.flagBindHTTP, config.HTTPBindAddr); err != nil { 189 + return nil, err 190 + } 191 + } 192 + if c.flagBindSerf != "" { 193 + if err := parseFlagAddress(c.flagBindSerf, config.SerfBindAddr); err != nil { 194 + return nil, err 195 + } 196 + } 197 + if c.flagJoin != "" { 198 + // TODO: moar validation 199 + config.StartJoin = strings.Split(c.flagJoin, ",") 200 + } 201 + 202 + if c.flagNode != "" { 203 + config.NodeName = c.flagNode 204 + } 205 + return config, nil 206 + }
+91
internal/cli/members.go
··· 1 + package cli 2 + 3 + import ( 4 + "bytes" 5 + "flag" 6 + "fmt" 7 + "os" 8 + "strings" 9 + "text/tabwriter" 10 + 11 + "git.j3s.sh/cascade/api" 12 + ) 13 + 14 + type membersCommand struct { 15 + usage string 16 + 17 + flagAPIAddr string 18 + flagDetails bool 19 + } 20 + 21 + func (c membersCommand) Usage() { 22 + fmt.Printf(`usage: cascade members [flags] 23 + list the members of the cascade serf cluster 24 + 25 + flags: 26 + -api 27 + address of the cascade http api to target (default = 127.0.0.1:8500) 28 + 29 + -details | -l 30 + show more details (default = nil) 31 + `) 32 + } 33 + 34 + func (c *membersCommand) Init(args []string) { 35 + flags := flag.NewFlagSet("", flag.ContinueOnError) 36 + flags.Usage = c.Usage 37 + flags.StringVar(&c.flagAPIAddr, "api", "", "") 38 + flags.BoolVar(&c.flagDetails, "details", false, "") 39 + flags.BoolVar(&c.flagDetails, "l", false, "") 40 + if err := flags.Parse(args); err != nil { 41 + fmt.Println(err) 42 + os.Exit(1) 43 + } 44 + } 45 + 46 + func RunMembers(args []string) { 47 + c := membersCommand{} 48 + c.Init(args) 49 + 50 + cfg := api.DefaultConfig() 51 + client, err := api.NewClient(cfg) 52 + if err != nil { 53 + fmt.Println(err) 54 + os.Exit(1) 55 + } 56 + members, err := client.Agent().Members() 57 + if err != nil { 58 + fmt.Println(err) 59 + os.Exit(1) 60 + } 61 + 62 + if c.flagDetails { 63 + var b bytes.Buffer 64 + tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) 65 + fmt.Fprintf(tw, "node\taddr\tstatus\ttags\n") 66 + for _, m := range members { 67 + fmt.Fprintf(tw, "%s\t", m.Name) 68 + fmt.Fprintf(tw, "%s:%d\t", m.Addr, m.Port) 69 + fmt.Fprintf(tw, "%s\t", m.StatusPretty()) 70 + fmt.Fprintf(tw, "%s\t", c.printTags(m.Tags)) 71 + fmt.Fprintln(tw) 72 + } 73 + if err := tw.Flush(); err != nil { 74 + fmt.Printf("error flushing tabwriter: %s", err) 75 + os.Exit(1) 76 + } 77 + fmt.Print(b.String()) 78 + } else { 79 + for _, m := range members { 80 + fmt.Println(m.Name) 81 + } 82 + } 83 + } 84 + 85 + func (c membersCommand) printTags(tags map[string]string) string { 86 + var results []string 87 + for k, v := range tags { 88 + results = append(results, fmt.Sprintf("%s=%s", k, v)) 89 + } 90 + return strings.Join(results, ",") 91 + }
+44
internal/cli/utils.go
··· 1 + package cli 2 + 3 + import ( 4 + "fmt" 5 + "net" 6 + "strconv" 7 + "strings" 8 + ) 9 + 10 + // parseFlagAddress takes a colon-delimited host:port pair as a string, parses 11 + // out the ip (and optionally, the port), and modifies the passed TCPAddr with 12 + // the resulting values. 13 + func parseFlagAddress(hostPort string, tcpAddr *net.TCPAddr) error { 14 + addr, portStr, err := net.SplitHostPort(hostPort) 15 + if err != nil { 16 + if !strings.Contains(err.Error(), "missing port in address") { 17 + return fmt.Errorf("Error parsing address: %v", err) 18 + } 19 + 20 + // If we get a missing port error, we try to coerce the whole hostPort 21 + // into an address. This allows the user to supply just a host address 22 + // instead of always requiring a host:ip pair. 23 + addr = hostPort 24 + } 25 + 26 + if addr == "" { 27 + return fmt.Errorf("Error parsing blank address") 28 + } 29 + ip := net.ParseIP(addr) 30 + if ip == nil { 31 + return fmt.Errorf("Error parsing address %q: not a valid IP address", ip) 32 + } 33 + 34 + if portStr != "" { 35 + port, err := strconv.Atoi(portStr) 36 + if err != nil { 37 + return fmt.Errorf("Error parsing port: %s", err) 38 + } 39 + tcpAddr.Port = port 40 + } 41 + tcpAddr.IP = ip 42 + 43 + return nil 44 + }
+11 -8
main.go
··· 4 4 "fmt" 5 5 "os" 6 6 7 - "git.j3s.sh/cascade/command/agent" 8 - "git.j3s.sh/cascade/command/ls" 7 + "git.j3s.sh/cascade/internal/cli" 9 8 ) 10 9 11 - const usage = `cascade agent start a cascade agent 12 - cascade ls list nodes, services, or members 13 - cascade rtt estimate latency between nodes 10 + const usage = ` usage: cascade [command] [flags] 11 + 12 + commands: 13 + cascade agent start a cascade agent 14 + cascade members list cluster members 15 + cascade status overview of the cascade cluster 16 + cascade rtt estimate latency between nodes 14 17 ` 15 18 16 19 // TODO: rename agent to something cooler ··· 24 27 args := os.Args[2:] 25 28 switch os.Args[1] { 26 29 case "agent": 27 - agent.Run(args) 28 - case "ls": 29 - ls.Run(args) 30 + cli.RunAgent(args) 31 + case "members": 32 + cli.RunMembers(args) 30 33 default: 31 34 fmt.Fprintf(os.Stderr, "'%s' is not a valid command\n\n%s", command, usage) 32 35 os.Exit(1)