leaderless gossip-based service discovery w/ consul compatibility
5

Configure Feed

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

Add address parsing logic to cmd/agent, test the shit out of it

Jes Olson (Feb 20, 2023, 3:18 PM -0800) f0ccb2a5 60a2e199

+282 -6
+4 -3
agent/agent.go
··· 58 58 "sync" 59 59 "time" 60 60 61 - "github.com/hashicorp/memberlist" 62 61 "github.com/hashicorp/serf/serf" 63 62 "golang.org/x/exp/slog" 64 63 ) ··· 86 85 func New(config *Config) *Agent { 87 86 agent := Agent{} 88 87 serfConfig := serf.DefaultConfig() 89 - serfConfig.MemberlistConfig = memberlist.DefaultWANConfig() 90 88 91 - // XXX: why do serf and cascade use the same event channel? 92 89 eventCh := make(chan serf.Event, 1024) 90 + agent.eventCh = eventCh 91 + serfConfig.EventCh = eventCh 92 + 93 + // XXX: why do serf and cascade use the same event channel? 93 94 agent.eventCh = eventCh 94 95 serfConfig.EventCh = eventCh 95 96
+27 -2
agent/config.go
··· 1 1 package agent 2 2 3 3 import ( 4 + "fmt" 4 5 "net" 5 6 "os" 6 7 ) 7 8 8 - const DefaultBindPort int = 4443 9 + const DefaultSerfPort int = 4443 9 10 const DefaultClientPort int = 4444 10 11 11 12 func DefaultConfig() *Config { ··· 16 17 17 18 // TODO: figure out how to default the listeners 18 19 cfg := Config{} 19 - cfg.BindAddr = &net.TCPAddr{IP: []byte{0, 0, 0, 0}, Port: DefaultBindPort} 20 + cfg.BindAddr = &net.TCPAddr{IP: []byte{0, 0, 0, 0}, Port: DefaultSerfPort} 20 21 cfg.ClientAddr = &net.TCPAddr{IP: []byte{127, 0, 0, 1}, Port: DefaultClientPort} 21 22 cfg.NodeName = hostname 22 23 ··· 29 30 NodeName string 30 31 StartJoin []string 31 32 } 33 + 34 + // BindAddrParts returns the parts of the BindAddr that should be 35 + // used to configure Serf. 36 + func (c *Config) AddrParts(address string) (string, int, error) { 37 + checkAddr := address 38 + 39 + START: 40 + _, _, err := net.SplitHostPort(checkAddr) 41 + if ae, ok := err.(*net.AddrError); ok && ae.Err == "missing port in address" { 42 + checkAddr = fmt.Sprintf("%s:%d", checkAddr, DefaultSerfPort) 43 + goto START 44 + } 45 + if err != nil { 46 + return "", 0, err 47 + } 48 + 49 + // Get the address 50 + addr, err := net.ResolveTCPAddr("tcp", checkAddr) 51 + if err != nil { 52 + return "", 0, err 53 + } 54 + 55 + return addr.IP.String(), addr.Port, nil 56 + }
api/api.go

This is a binary file and will not be displayed.

+162
command/agent/agent.go
··· 1 + package agent 2 + 3 + import ( 4 + "fmt" 5 + "log" 6 + "net" 7 + "os" 8 + "os/signal" 9 + "strconv" 10 + "strings" 11 + "syscall" 12 + "time" 13 + 14 + "git.j3s.sh/cascade/agent" 15 + ) 16 + 17 + // gracefulTimeout controls how long we wait before forcefully terminating 18 + // note that this value interacts with serf's LeavePropagateDelay config 19 + const gracefulTimeout = 10 * time.Second 20 + 21 + func Run(args []string) { 22 + // do flags 23 + config := configureAgent() 24 + agent := agent.New(config) 25 + if err := agent.Start(); err != nil { 26 + fmt.Println(err) 27 + os.Exit(1) 28 + } 29 + defer agent.Shutdown() 30 + // join any specified startup nodes 31 + if err := startupJoin(agent); err != nil { 32 + fmt.Println(err) 33 + os.Exit(1) 34 + } 35 + if err := handleSignals(agent); err != nil { 36 + fmt.Println(err) 37 + os.Exit(1) 38 + } 39 + } 40 + 41 + // handleSignals blocks until we get an exit-causing signal 42 + func handleSignals(agent *agent.Agent) error { 43 + signalCh := make(chan os.Signal, 4) 44 + signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) 45 + 46 + // Wait for a signal 47 + var sig os.Signal 48 + select { 49 + case s := <-signalCh: 50 + sig = s 51 + case <-agent.ShutdownCh(): 52 + // Agent is already shutdown! 53 + return nil 54 + } 55 + fmt.Fprintf(os.Stderr, "caught signal %s", sig) 56 + 57 + // Check if we should do a graceful leave 58 + graceful := false 59 + if sig == os.Interrupt || sig == syscall.SIGTERM { 60 + graceful = true 61 + } 62 + 63 + // Bail fast if not doing a graceful leave 64 + if !graceful { 65 + fmt.Fprintf(os.Stderr, "leave cluster with zero grace") 66 + return nil 67 + } 68 + 69 + // Attempt a graceful leave 70 + gracefulCh := make(chan struct{}) 71 + fmt.Printf("shutting down gracefully") 72 + go func() { 73 + if err := agent.Leave(); err != nil { 74 + fmt.Println("Error: ", err) 75 + return 76 + } 77 + close(gracefulCh) 78 + }() 79 + 80 + // Wait for leave or another signal 81 + select { 82 + case <-signalCh: 83 + return fmt.Errorf("idfk") 84 + case <-time.After(gracefulTimeout): 85 + return fmt.Errorf("leave timed out") 86 + case <-gracefulCh: 87 + return nil 88 + } 89 + } 90 + 91 + func startupJoin(a *agent.Agent) error { 92 + if len(a.Config.StartJoin) == 0 { 93 + return nil 94 + } 95 + 96 + n, err := a.Join(a.Config.StartJoin) 97 + if err != nil { 98 + return err 99 + } 100 + if n > 0 { 101 + log.Printf("issue join request nodes=%d\n", n) 102 + } 103 + 104 + return nil 105 + } 106 + 107 + func configureAgent() *agent.Config { 108 + config := agent.DefaultConfig() 109 + // CASCADE_BIND=192.168.0.15:12345 110 + if os.Getenv("CASCADE_BIND") != "" { 111 + err := parseFlagAddress(os.Getenv("CASCADE_BIND"), config.BindAddr) 112 + if err != nil { 113 + fmt.Printf("Error parsing CASCADE_BIND: %s\n", err) 114 + os.Exit(1) 115 + } 116 + } 117 + // CASCADE_JOIN=127.0.0.1,127.0.0.5 118 + if os.Getenv("CASCADE_JOIN") != "" { 119 + config.StartJoin = strings.Split(os.Getenv("CASCADE_JOIN"), ",") 120 + } 121 + // CASCADE_NAME=nostromo.j3s.sh 122 + if os.Getenv("CASCADE_NAME") != "" { 123 + config.NodeName = os.Getenv("CASCADE_NAME") 124 + } 125 + return config 126 + } 127 + 128 + // parseFlagAddress takes a colon-delimited host:port pair as a string, parses 129 + // out the ip (and optionally, the port), and modifies the passed TCPAddr with 130 + // the resulting values. 131 + func parseFlagAddress(hostPort string, tcpAddr *net.TCPAddr) error { 132 + addr, portStr, err := net.SplitHostPort(hostPort) 133 + if err != nil { 134 + if !strings.Contains(err.Error(), "missing port in address") { 135 + return fmt.Errorf("Error parsing address: %v", err) 136 + } 137 + 138 + // If we get a missing port error, we try to coerce the whole hostPort 139 + // into an address. This allows the user to supply just a host address 140 + // instead of always requiring a host:ip pair. 141 + addr = hostPort 142 + } 143 + 144 + if addr == "" { 145 + return fmt.Errorf("Error parsing blank address") 146 + } 147 + ip := net.ParseIP(addr) 148 + if ip == nil { 149 + return fmt.Errorf("Error parsing address %q: not a valid IP address", ip) 150 + } 151 + 152 + if portStr != "" { 153 + port, err := strconv.Atoi(portStr) 154 + if err != nil { 155 + return fmt.Errorf("Error parsing port: %s", err) 156 + } 157 + tcpAddr.Port = port 158 + } 159 + tcpAddr.IP = ip 160 + 161 + return nil 162 + }
+89
command/agent/agent_test.go
··· 1 + package agent 2 + 3 + import ( 4 + "strings" 5 + "testing" 6 + 7 + "git.j3s.sh/cascade/agent" 8 + ) 9 + 10 + func TestParseFlagAddress(t *testing.T) { 11 + type c struct { 12 + inputAddr string 13 + expectIP string 14 + expectPort int 15 + expectErrStr string 16 + } 17 + cases := []c{ 18 + { 19 + inputAddr: "127.0.0.1", 20 + expectIP: "127.0.0.1", 21 + expectPort: 4443, 22 + }, 23 + { 24 + inputAddr: "127.0.0.1:6969", 25 + expectIP: "127.0.0.1", 26 + expectPort: 6969, 27 + }, 28 + { 29 + inputAddr: "192.168.0.4:420", 30 + expectIP: "192.168.0.4", 31 + expectPort: 420, 32 + }, 33 + { 34 + inputAddr: "127.0.0.3:", 35 + expectIP: "127.0.0.3", 36 + expectPort: 4443, 37 + }, 38 + // error cases 39 + { 40 + inputAddr: "", 41 + expectIP: "0.0.0.0", 42 + expectPort: 4443, 43 + expectErrStr: "Error parsing blank address", 44 + }, 45 + { 46 + inputAddr: ":1234", 47 + expectIP: "0.0.0.0", 48 + expectPort: 4443, 49 + expectErrStr: "Error parsing blank address", 50 + }, 51 + { 52 + inputAddr: "127.0.0.1:abcd", 53 + expectIP: "0.0.0.0", 54 + expectPort: 4443, 55 + expectErrStr: "Error parsing port", 56 + }, 57 + { 58 + inputAddr: "127.0.0.256:6969", 59 + expectIP: "0.0.0.0", 60 + expectPort: 4443, 61 + expectErrStr: "Error parsing address", 62 + }, 63 + } 64 + 65 + for _, tc := range cases { 66 + addr := agent.DefaultConfig().BindAddr 67 + expectErr := tc.expectErrStr != "" 68 + 69 + err := parseFlagAddress(tc.inputAddr, addr) 70 + if expectErr && err == nil { 71 + t.Errorf("Expected error '%s', but received none", tc.expectErrStr) 72 + } 73 + if !expectErr && err != nil { 74 + t.Errorf("Unexpected error: '%s'", err) 75 + } 76 + // errors we expect are unwrapped here 77 + if expectErr && err != nil { 78 + if !strings.Contains(err.Error(), tc.expectErrStr) { 79 + t.Errorf("Expected error '%s', got '%s'", tc.expectErrStr, err) 80 + } 81 + } 82 + if tc.expectIP != addr.IP.String() { 83 + t.Errorf("Expected IP '%s', got '%s'", tc.expectIP, addr.IP.String()) 84 + } 85 + if tc.expectPort != addr.Port { 86 + t.Errorf("Expected port '%d', got '%d'", tc.expectPort, addr.Port) 87 + } 88 + } 89 + }
-1
main.go
··· 48 48 49 49 func handleAgent() { 50 50 config := getAgentConfig() 51 - fmt.Printf("%+v", config) 52 51 agent := agent.New(config) 53 52 if err := agent.Start(); err != nil { 54 53 fmt.Println(err)