···11+package api
22+33+import "fmt"
44+55+// Node is a Consul-shaped representation of a cluster member.
66+type Node struct {
77+ Node string
88+ Address string
99+ Meta map[string]string
1010+}
1111+1212+// CatalogService describes a single service instance as exposed by the
1313+// /v1/catalog/service/<name> endpoint.
1414+type CatalogService struct {
1515+ Node string
1616+ Address string
1717+ ServiceID string
1818+ ServiceName string
1919+ ServiceAddress string
2020+ ServicePort int
2121+ ServiceTags []string
2222+ ServiceMeta map[string]string
2323+}
2424+2525+// CatalogNode bundles a node and the services it owns, returned by
2626+// /v1/catalog/node/<name>.
2727+type CatalogNode struct {
2828+ Node *Node
2929+ Services map[string]*AgentService
3030+}
3131+3232+// Catalog provides access to the cluster-wide read views over gossipped
3333+// service state.
3434+type Catalog struct {
3535+ c *Client
3636+}
3737+3838+// Catalog returns a handle to the catalog endpoints.
3939+func (c *Client) Catalog() *Catalog {
4040+ return &Catalog{c: c}
4141+}
4242+4343+// Services returns the cluster-wide service-name -> tag-union view.
4444+func (cat *Catalog) Services() (map[string][]string, error) {
4545+ r := cat.c.newRequest("GET", "/v1/catalog/services")
4646+ _, resp, err := requireOK(cat.c.doRequest(r))
4747+ if err != nil {
4848+ return nil, err
4949+ }
5050+ defer closeResponseBody(resp)
5151+ var out map[string][]string
5252+ if err := decodeJSONBody(resp.Body, &out); err != nil {
5353+ return nil, err
5454+ }
5555+ return out, nil
5656+}
5757+5858+// Service returns every known instance of the named service.
5959+func (cat *Catalog) Service(name string) ([]*CatalogService, error) {
6060+ r := cat.c.newRequest("GET", fmt.Sprintf("/v1/catalog/service/%s", name))
6161+ _, resp, err := requireOK(cat.c.doRequest(r))
6262+ if err != nil {
6363+ return nil, err
6464+ }
6565+ defer closeResponseBody(resp)
6666+ var out []*CatalogService
6767+ if err := decodeJSONBody(resp.Body, &out); err != nil {
6868+ return nil, err
6969+ }
7070+ return out, nil
7171+}
7272+7373+// Nodes returns every node serf knows about.
7474+func (cat *Catalog) Nodes() ([]*Node, error) {
7575+ r := cat.c.newRequest("GET", "/v1/catalog/nodes")
7676+ _, resp, err := requireOK(cat.c.doRequest(r))
7777+ if err != nil {
7878+ return nil, err
7979+ }
8080+ defer closeResponseBody(resp)
8181+ var out []*Node
8282+ if err := decodeJSONBody(resp.Body, &out); err != nil {
8383+ return nil, err
8484+ }
8585+ return out, nil
8686+}
8787+8888+// Node returns the node and the services it owns.
8989+func (cat *Catalog) Node(name string) (*CatalogNode, error) {
9090+ r := cat.c.newRequest("GET", fmt.Sprintf("/v1/catalog/node/%s", name))
9191+ _, resp, err := requireOK(cat.c.doRequest(r))
9292+ if err != nil {
9393+ return nil, err
9494+ }
9595+ defer closeResponseBody(resp)
9696+ var out CatalogNode
9797+ if err := decodeJSONBody(resp.Body, &out); err != nil {
9898+ return nil, err
9999+ }
100100+ return &out, nil
101101+}
+45
internal/agent/agent.go
···231231 return a.services.Owned()
232232}
233233234234+// CatalogServices returns the cluster-wide service-name -> tag-union view.
235235+func (a *Agent) CatalogServices() map[string][]string {
236236+ return a.services.AllServices()
237237+}
238238+239239+// CatalogServiceInstances returns every known instance of the named service.
240240+func (a *Agent) CatalogServiceInstances(name string) []NodeService {
241241+ return a.services.ServiceInstances(name)
242242+}
243243+244244+// CatalogNodeServices returns the services owned by the named node.
245245+func (a *Agent) CatalogNodeServices(node string) map[string]*api.AgentService {
246246+ return a.services.NodeServices(node)
247247+}
248248+249249+// CatalogNodes returns every node serf currently knows about, regardless of
250250+// whether it has registered services. Nodes that have gossipped services
251251+// but are not yet visible to serf (rare race during join) are also included.
252252+func (a *Agent) CatalogNodes() []serf.Member {
253253+ members := a.serf.Members()
254254+ seen := map[string]struct{}{}
255255+ for _, m := range members {
256256+ seen[m.Name] = struct{}{}
257257+ }
258258+ for _, n := range a.services.Nodes() {
259259+ if _, ok := seen[n]; ok {
260260+ continue
261261+ }
262262+ members = append(members, serf.Member{Name: n, Status: serf.StatusNone})
263263+ seen[n] = struct{}{}
264264+ }
265265+ return members
266266+}
267267+268268+// MemberByName returns the serf member with the given name, or zero value
269269+// if no such member is known.
270270+func (a *Agent) MemberByName(name string) (serf.Member, bool) {
271271+ for _, m := range a.serf.Members() {
272272+ if m.Name == name {
273273+ return m, true
274274+ }
275275+ }
276276+ return serf.Member{}, false
277277+}
278278+234279// antiEntropyLoop periodically re-broadcasts this agent's owned services so
235280// peers that missed the original user event eventually catch up.
236281func (a *Agent) antiEntropyLoop() {
+106
internal/agent/catalog_endpoint.go
···11+package agent
22+33+import (
44+ "fmt"
55+ "net/http"
66+ "strings"
77+88+ "git.j3s.sh/cascade/api"
99+)
1010+1111+// catalogServices serves GET /v1/catalog/services. Returns the cluster-wide
1212+// map of service name -> union of tags seen on any instance.
1313+func (s *HTTPHandlers) catalogServices(w http.ResponseWriter, r *http.Request) {
1414+ out := s.agent.CatalogServices()
1515+ if out == nil {
1616+ out = map[string][]string{}
1717+ }
1818+ s.writeJSON(w, r, out)
1919+}
2020+2121+// catalogService serves GET /v1/catalog/service/<name>. Returns one entry
2222+// per known instance, joined with the owning node's serf membership info.
2323+func (s *HTTPHandlers) catalogService(w http.ResponseWriter, r *http.Request) {
2424+ name := strings.TrimPrefix(r.URL.Path, "/v1/catalog/service/")
2525+ if name == "" || strings.Contains(name, "/") {
2626+ http.Error(w, "service name required in path", http.StatusBadRequest)
2727+ return
2828+ }
2929+3030+ instances := s.agent.CatalogServiceInstances(name)
3131+ out := make([]api.CatalogService, 0, len(instances))
3232+ for _, inst := range instances {
3333+ entry := api.CatalogService{
3434+ Node: inst.Node,
3535+ ServiceID: inst.Service.ID,
3636+ ServiceName: inst.Service.Service,
3737+ ServiceAddress: inst.Service.Address,
3838+ ServicePort: inst.Service.Port,
3939+ ServiceTags: inst.Service.Tags,
4040+ ServiceMeta: inst.Service.Meta,
4141+ }
4242+ if m, ok := s.agent.MemberByName(inst.Node); ok {
4343+ entry.Address = m.Addr.String()
4444+ }
4545+ out = append(out, entry)
4646+ }
4747+ s.writeJSON(w, r, out)
4848+}
4949+5050+// catalogNodes serves GET /v1/catalog/nodes. Returns the serf membership
5151+// list shaped as Consul-style Node entries.
5252+func (s *HTTPHandlers) catalogNodes(w http.ResponseWriter, r *http.Request) {
5353+ members := s.agent.CatalogNodes()
5454+ out := make([]api.Node, 0, len(members))
5555+ for _, m := range members {
5656+ out = append(out, api.Node{
5757+ Node: m.Name,
5858+ Address: m.Addr.String(),
5959+ Meta: m.Tags,
6060+ })
6161+ }
6262+ s.writeJSON(w, r, out)
6363+}
6464+6565+// catalogNode serves GET /v1/catalog/node/<name>. Returns the node and the
6666+// services it owns.
6767+func (s *HTTPHandlers) catalogNode(w http.ResponseWriter, r *http.Request) {
6868+ name := strings.TrimPrefix(r.URL.Path, "/v1/catalog/node/")
6969+ if name == "" || strings.Contains(name, "/") {
7070+ http.Error(w, "node name required in path", http.StatusBadRequest)
7171+ return
7272+ }
7373+7474+ out := api.CatalogNode{
7575+ Services: s.agent.CatalogNodeServices(name),
7676+ }
7777+ if out.Services == nil {
7878+ out.Services = map[string]*api.AgentService{}
7979+ }
8080+ if m, ok := s.agent.MemberByName(name); ok {
8181+ out.Node = &api.Node{
8282+ Node: m.Name,
8383+ Address: m.Addr.String(),
8484+ Meta: m.Tags,
8585+ }
8686+ } else if len(out.Services) > 0 {
8787+ // Node has gossipped services but serf hasn't surfaced it yet.
8888+ out.Node = &api.Node{Node: name}
8989+ } else {
9090+ http.Error(w, fmt.Sprintf("unknown node %q", name), http.StatusNotFound)
9191+ return
9292+ }
9393+ s.writeJSON(w, r, out)
9494+}
9595+9696+// writeJSON is the standard 200 + application/json response helper.
9797+func (s *HTTPHandlers) writeJSON(w http.ResponseWriter, r *http.Request, obj interface{}) {
9898+ buf, err := s.marshalJSON(r, obj)
9999+ if err != nil {
100100+ s.agent.logger.Error("marshal response", err)
101101+ w.WriteHeader(http.StatusInternalServerError)
102102+ return
103103+ }
104104+ w.Header().Set("Content-Type", "application/json")
105105+ w.Write(buf)
106106+}
+8-5
internal/agent/http.go
···110110111111 mux.HandleFunc("/", s.Index)
112112 endpoints := map[string]func(resp http.ResponseWriter, req *http.Request){
113113- "/v1/agent/members": s.agentMembers,
114114- "/v1/agent/self": s.agentSelf,
115115- "/v1/agent/services": s.agentServices,
116116- "/v1/agent/service/register": s.agentServiceRegister,
113113+ "/v1/agent/members": s.agentMembers,
114114+ "/v1/agent/self": s.agentSelf,
115115+ "/v1/agent/services": s.agentServices,
116116+ "/v1/agent/service/register": s.agentServiceRegister,
117117 "/v1/agent/service/deregister/": s.agentServiceDeregister,
118118+ "/v1/catalog/services": s.catalogServices,
119119+ "/v1/catalog/service/": s.catalogService,
120120+ "/v1/catalog/nodes": s.catalogNodes,
121121+ "/v1/catalog/node/": s.catalogNode,
118122 }
119123120124 for pattern, fn := range endpoints {
···246250 // Give them something helpful if there's no UI so they at least know
247251 // what this server is.
248252 fmt.Fprint(resp, "cascade agent\n")
249249- return
250253}
251254252255func decodeBody(body io.Reader, out interface{}) error {
+87
internal/agent/services.go
···33import (
44 "encoding/json"
55 "fmt"
66+ "sort"
67 "sync"
7889 "git.j3s.sh/cascade/api"
···171172 cp := *svc
172173 out[id] = &cp
173174 }
175175+ return out
176176+}
177177+178178+// NodeService pairs a service with the node that owns it.
179179+type NodeService struct {
180180+ Node string
181181+ Service *api.AgentService
182182+}
183183+184184+// AllServices returns the cluster-wide map of service name to the union of
185185+// tags seen across all instances. The empty slice means the service exists
186186+// with no tags somewhere.
187187+func (s *ServiceStore) AllServices() map[string][]string {
188188+ s.mu.RLock()
189189+ defer s.mu.RUnlock()
190190+ tagsByName := map[string]map[string]struct{}{}
191191+ for _, byID := range s.services {
192192+ for _, svc := range byID {
193193+ set, ok := tagsByName[svc.Service]
194194+ if !ok {
195195+ set = map[string]struct{}{}
196196+ tagsByName[svc.Service] = set
197197+ }
198198+ for _, t := range svc.Tags {
199199+ set[t] = struct{}{}
200200+ }
201201+ }
202202+ }
203203+ out := make(map[string][]string, len(tagsByName))
204204+ for name, set := range tagsByName {
205205+ tags := make([]string, 0, len(set))
206206+ for t := range set {
207207+ tags = append(tags, t)
208208+ }
209209+ sort.Strings(tags)
210210+ out[name] = tags
211211+ }
212212+ return out
213213+}
214214+215215+// ServiceInstances returns every gossipped instance of the named service,
216216+// across all nodes.
217217+func (s *ServiceStore) ServiceInstances(name string) []NodeService {
218218+ s.mu.RLock()
219219+ defer s.mu.RUnlock()
220220+ var out []NodeService
221221+ for node, byID := range s.services {
222222+ for _, svc := range byID {
223223+ if svc.Service != name {
224224+ continue
225225+ }
226226+ cp := *svc
227227+ out = append(out, NodeService{Node: node, Service: &cp})
228228+ }
229229+ }
230230+ sort.Slice(out, func(i, j int) bool {
231231+ if out[i].Node != out[j].Node {
232232+ return out[i].Node < out[j].Node
233233+ }
234234+ return out[i].Service.ID < out[j].Service.ID
235235+ })
236236+ return out
237237+}
238238+239239+// NodeServices returns every service owned by the named node.
240240+func (s *ServiceStore) NodeServices(node string) map[string]*api.AgentService {
241241+ s.mu.RLock()
242242+ defer s.mu.RUnlock()
243243+ out := map[string]*api.AgentService{}
244244+ for id, svc := range s.services[node] {
245245+ cp := *svc
246246+ out[id] = &cp
247247+ }
248248+ return out
249249+}
250250+251251+// Nodes returns the set of node names that currently have at least one
252252+// gossipped service.
253253+func (s *ServiceStore) Nodes() []string {
254254+ s.mu.RLock()
255255+ defer s.mu.RUnlock()
256256+ out := make([]string, 0, len(s.services))
257257+ for node := range s.services {
258258+ out = append(out, node)
259259+ }
260260+ sort.Strings(out)
174261 return out
175262}
176263
+21-17
internal/cli/members.go
internal/cli/nodes.go
···1111 "git.j3s.sh/cascade/api"
1212)
13131414-type membersCommand struct {
1515- usage string
1616-1414+type nodesCommand struct {
1715 flagAPIAddr string
1816 flagDetails bool
1917}
20182121-func (c membersCommand) Usage() {
2222- fmt.Printf(`usage: cascade members [flags]
2323- list the members of the cascade serf cluster
2424-1919+func (c nodesCommand) Usage() {
2020+ fmt.Printf(`usage: cascade nodes [flags]
2121+ list the nodes in the cascade cluster.
2222+2523flags:
2624 -api
2725 address of the cascade http api to target (default = 127.0.0.1:8500)
2826`)
2927}
30283131-func (c *membersCommand) Init(args []string) {
2929+func (c *nodesCommand) Init(args []string) {
3230 flags := flag.NewFlagSet("", flag.ContinueOnError)
3331 flags.Usage = c.Usage
3432 flags.StringVar(&c.flagAPIAddr, "api", "", "")
···4038 }
4139}
42404343-func RunMembers(args []string) {
4444- c := membersCommand{}
4141+func RunNodes(args []string) {
4242+ c := nodesCommand{}
4543 c.Init(args)
46444745 cfg := api.DefaultConfig()
4646+ if c.flagAPIAddr != "" {
4747+ cfg.Address = c.flagAPIAddr
4848+ }
4849 client, err := api.NewClient(cfg)
4950 if err != nil {
5051 fmt.Println(err)
5152 os.Exit(1)
5253 }
5353- members, err := client.Agent().Members()
5454+ nodes, err := client.Agent().Members()
5455 if err != nil {
5556 fmt.Println(err)
5657 os.Exit(1)
···5960 var b bytes.Buffer
6061 tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
6162 fmt.Fprintf(tw, "node\taddr\tstatus\ttags\n")
6262- for _, m := range members {
6363- fmt.Fprintf(tw, "%s\t", m.Name)
6464- fmt.Fprintf(tw, "%s:%d\t", m.Addr, m.Port)
6565- fmt.Fprintf(tw, "%s\t", m.StatusPretty())
6666- fmt.Fprintf(tw, "%s\t", c.printTags(m.Tags))
6363+ for _, n := range nodes {
6464+ fmt.Fprintf(tw, "%s\t", n.Name)
6565+ fmt.Fprintf(tw, "%s:%d\t", n.Addr, n.Port)
6666+ fmt.Fprintf(tw, "%s\t", n.StatusPretty())
6767+ fmt.Fprintf(tw, "%s\t", c.printTags(n.Tags))
6768 fmt.Fprintln(tw)
6869 }
6970 if err := tw.Flush(); err != nil {
···7374 fmt.Print(b.String())
7475}
75767676-func (c membersCommand) printTags(tags map[string]string) string {
7777+func (c nodesCommand) printTags(tags map[string]string) string {
7778 var results []string
7879 for k, v := range tags {
7980 results = append(results, fmt.Sprintf("%s=%s", k, v))
8181+ }
8282+ if len(results) == 0 {
8383+ return "<none>"
8084 }
8185 return strings.Join(results, ",")
8286}
+307-22
internal/cli/services.go
···11package cli
2233import (
44+ "bufio"
45 "bytes"
66+ "encoding/json"
57 "flag"
68 "fmt"
99+ "io"
710 "net"
811 "os"
912 "sort"
···1922}
20232124func (c servicesCommand) Usage() {
2222- fmt.Printf(`usage: cascade services [flags]
2323- list the services registered to a cascade agent.
2525+ fmt.Printf(`usage: cascade services [name] [flags]
2626+ with no name, list every service in the cluster.
2727+ with a name, list every instance of that service.
24282529flags:
2630 -api
···2832`)
2933}
30343131-func (c *servicesCommand) Init(args []string) {
3535+func (c *servicesCommand) Init(args []string) []string {
3236 flags := flag.NewFlagSet("", flag.ContinueOnError)
3337 flags.Usage = c.Usage
3438 flags.StringVar(&c.flagAPIAddr, "api", "", "")
···3640 fmt.Println(err)
3741 os.Exit(1)
3842 }
4343+ return flags.Args()
3944}
40454146func RunServices(args []string) {
4247 c := servicesCommand{}
4343- c.Init(args)
4848+ rest := c.Init(args)
44494550 cfg := api.DefaultConfig()
4651 if c.flagAPIAddr != "" {
···5257 os.Exit(1)
5358 }
54595555- services, err := client.Agent().Services()
6060+ if len(rest) == 0 {
6161+ listAllServices(client)
6262+ return
6363+ }
6464+ listServiceInstances(client, rest[0])
6565+}
6666+6767+func listAllServices(client *api.Client) {
6868+ services, err := client.Catalog().Services()
5669 if err != nil {
5770 fmt.Println(err)
5871 os.Exit(1)
5972 }
6060-6173 if len(services) == 0 {
6262- fmt.Printf("no services registered on %s\n", cfg.Address)
7474+ fmt.Println("no services known in the cluster")
6375 return
6476 }
65776666- ids := make([]string, 0, len(services))
6767- for id := range services {
6868- ids = append(ids, id)
7878+ names := make([]string, 0, len(services))
7979+ for n := range services {
8080+ names = append(names, n)
8181+ }
8282+ sort.Strings(names)
8383+8484+ var b bytes.Buffer
8585+ tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
8686+ fmt.Fprintln(tw, "service\ttags")
8787+ for _, n := range names {
8888+ fmt.Fprintf(tw, "%s\t%s\n", n, joinTagsOrNone(services[n]))
6989 }
7070- sort.Strings(ids)
9090+ tw.Flush()
9191+ fmt.Print(b.String())
9292+}
71937272- nodeName, err := client.Agent().NodeName()
9494+func listServiceInstances(client *api.Client, name string) {
9595+ instances, err := client.Catalog().Service(name)
7396 if err != nil {
7497 fmt.Println(err)
7598 os.Exit(1)
7699 }
100100+ if len(instances) == 0 {
101101+ fmt.Printf("no instances of %q known in the cluster\n", name)
102102+ return
103103+ }
104104+77105 var b bytes.Buffer
78106 tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
7979- fmt.Fprintf(tw, "node\tid\tname\taddr\ttags\n")
8080- for _, id := range ids {
8181- s := services[id]
8282- addr := s.Address
8383- if s.Port != 0 {
8484- addr = net.JoinHostPort(addr, strconv.Itoa(s.Port))
107107+ fmt.Fprintln(tw, "node\tid\taddr\ttags")
108108+ for _, inst := range instances {
109109+ addr := inst.ServiceAddress
110110+ if addr == "" {
111111+ addr = inst.Address
112112+ }
113113+ if inst.ServicePort != 0 {
114114+ addr = net.JoinHostPort(addr, strconv.Itoa(inst.ServicePort))
85115 }
8686- fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", nodeName, s.ID, s.Service, addr, strings.Join(s.Tags, ","))
116116+ fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", inst.Node, inst.ServiceID, addr, joinTagsOrNone(inst.ServiceTags))
117117+ }
118118+ tw.Flush()
119119+ fmt.Print(b.String())
120120+}
121121+122122+type registerCmd struct {
123123+ flagAPIAddr string
124124+ flagName string
125125+ flagID string
126126+ flagAddr string
127127+ flagPort int
128128+ flagTags string
129129+ flagFile string
130130+}
131131+132132+func (c registerCmd) Usage() {
133133+ fmt.Printf(`usage: cascade register [flags]
134134+ register a service on the local cascade agent. with no flags, prompts
135135+ interactively. with -f, reads a full AgentService JSON document from a
136136+ file (use '-' for stdin).
137137+138138+flags:
139139+ -api address of the cascade http api to target (default = 127.0.0.1:8500)
140140+ -name service name (required)
141141+ -port service port (required)
142142+ -id service id (default = name)
143143+ -addr service address (default = empty; consumers use the node address)
144144+ -tags comma-separated tags
145145+ -f read AgentService JSON from file or '-' for stdin
146146+`)
147147+}
148148+149149+func RunRegister(args []string) {
150150+ c := registerCmd{}
151151+ flags := flag.NewFlagSet("", flag.ContinueOnError)
152152+ flags.Usage = c.Usage
153153+ flags.StringVar(&c.flagAPIAddr, "api", "", "")
154154+ flags.StringVar(&c.flagName, "name", "", "")
155155+ flags.StringVar(&c.flagID, "id", "", "")
156156+ flags.StringVar(&c.flagAddr, "addr", "", "")
157157+ flags.IntVar(&c.flagPort, "port", 0, "")
158158+ flags.StringVar(&c.flagTags, "tags", "", "")
159159+ flags.StringVar(&c.flagFile, "f", "", "")
160160+ if err := flags.Parse(args); err != nil {
161161+ os.Exit(1)
162162+ }
163163+164164+ var svc *api.AgentService
165165+ var err error
166166+ switch {
167167+ case c.flagFile != "":
168168+ svc, err = readServiceFile(c.flagFile)
169169+ case c.flagName != "" && c.flagPort != 0:
170170+ svc = c.fromFlags()
171171+ default:
172172+ svc, err = c.prompt()
173173+ }
174174+ if err != nil {
175175+ fmt.Println(err)
176176+ os.Exit(1)
177177+ }
178178+ if svc.Service == "" {
179179+ fmt.Println("service name required")
180180+ os.Exit(1)
181181+ }
182182+ if svc.ID == "" {
183183+ svc.ID = svc.Service
184184+ }
185185+186186+ cfg := api.DefaultConfig()
187187+ if c.flagAPIAddr != "" {
188188+ cfg.Address = c.flagAPIAddr
189189+ }
190190+ client, err := api.NewClient(cfg)
191191+ if err != nil {
192192+ fmt.Println(err)
193193+ os.Exit(1)
194194+ }
195195+ if err := client.Agent().ServiceRegister(svc); err != nil {
196196+ fmt.Printf("register failed: %s\n", err)
197197+ os.Exit(1)
198198+ }
199199+200200+ addrDesc := svc.Address
201201+ if addrDesc == "" {
202202+ addrDesc = "<node addr>"
203203+ }
204204+ if svc.Port != 0 {
205205+ addrDesc = net.JoinHostPort(addrDesc, strconv.Itoa(svc.Port))
206206+ }
207207+ fmt.Printf("service registered: %s (%s)", svc.Service, addrDesc)
208208+ if len(svc.Tags) > 0 {
209209+ fmt.Printf(" tags=%s", strings.Join(svc.Tags, ","))
210210+ }
211211+ fmt.Printf("\n")
212212+}
213213+214214+func (c registerCmd) fromFlags() *api.AgentService {
215215+ svc := &api.AgentService{
216216+ ID: c.flagID,
217217+ Service: c.flagName,
218218+ Address: c.flagAddr,
219219+ Port: c.flagPort,
220220+ }
221221+ if c.flagTags != "" {
222222+ svc.Tags = splitTags(c.flagTags)
223223+ }
224224+ return svc
225225+}
226226+227227+func (c registerCmd) prompt() (*api.AgentService, error) {
228228+ if !isTerminal(os.Stdin) {
229229+ return nil, fmt.Errorf("register requires -name and -port (or -f) when stdin is not a tty")
230230+ }
231231+ r := bufio.NewReader(os.Stdin)
232232+233233+ name := promptLine(r, "service name", c.flagName)
234234+ if name == "" {
235235+ return nil, fmt.Errorf("service name required")
87236 }
8888- if err := tw.Flush(); err != nil {
8989- fmt.Printf("error flushing tabwriter: %s", err)
237237+238238+ portDef := ""
239239+ if c.flagPort != 0 {
240240+ portDef = strconv.Itoa(c.flagPort)
241241+ }
242242+ portStr := promptLine(r, "port", portDef)
243243+ port, err := strconv.Atoi(portStr)
244244+ if err != nil || port <= 0 {
245245+ return nil, fmt.Errorf("port must be a positive integer")
246246+ }
247247+248248+ id := promptLine(r, "id", firstNonEmpty(c.flagID, name))
249249+ addr := promptLine(r, "address (empty = use node addr)", c.flagAddr)
250250+ tags := promptLine(r, "tags (comma-separated)", c.flagTags)
251251+252252+ svc := &api.AgentService{
253253+ ID: id,
254254+ Service: name,
255255+ Address: addr,
256256+ Port: port,
257257+ }
258258+ if tags != "" {
259259+ svc.Tags = splitTags(tags)
260260+ }
261261+ return svc, nil
262262+}
263263+264264+type deregisterCmd struct {
265265+ flagAPIAddr string
266266+}
267267+268268+func (c deregisterCmd) Usage() {
269269+ fmt.Printf(`usage: cascade deregister <id> [flags]
270270+ remove a service from the local cascade agent.
271271+272272+flags:
273273+ -api address of the cascade http api to target (default = 127.0.0.1:8500)
274274+`)
275275+}
276276+277277+func RunDeregister(args []string) {
278278+ c := deregisterCmd{}
279279+ flags := flag.NewFlagSet("", flag.ContinueOnError)
280280+ flags.Usage = c.Usage
281281+ flags.StringVar(&c.flagAPIAddr, "api", "", "")
282282+ if err := flags.Parse(args); err != nil {
90283 os.Exit(1)
91284 }
9292- fmt.Print(b.String())
285285+ rest := flags.Args()
286286+ if len(rest) == 0 {
287287+ c.Usage()
288288+ os.Exit(1)
289289+ }
290290+ id := rest[0]
291291+292292+ cfg := api.DefaultConfig()
293293+ if c.flagAPIAddr != "" {
294294+ cfg.Address = c.flagAPIAddr
295295+ }
296296+ client, err := api.NewClient(cfg)
297297+ if err != nil {
298298+ fmt.Println(err)
299299+ os.Exit(1)
300300+ }
301301+ if err := client.Agent().ServiceDeregister(id); err != nil {
302302+ fmt.Printf("deregister failed: %s\n", err)
303303+ os.Exit(1)
304304+ }
305305+ fmt.Printf("deregistered %s\n", id)
306306+}
307307+308308+func readServiceFile(path string) (*api.AgentService, error) {
309309+ var r io.Reader
310310+ if path == "-" {
311311+ r = os.Stdin
312312+ } else {
313313+ f, err := os.Open(path)
314314+ if err != nil {
315315+ return nil, err
316316+ }
317317+ defer f.Close()
318318+ r = f
319319+ }
320320+ var svc api.AgentService
321321+ if err := json.NewDecoder(r).Decode(&svc); err != nil {
322322+ return nil, fmt.Errorf("decode %s: %w", path, err)
323323+ }
324324+ return &svc, nil
325325+}
326326+327327+func promptLine(r *bufio.Reader, label, def string) string {
328328+ if def != "" {
329329+ fmt.Printf("%s [%s]: ", label, def)
330330+ } else {
331331+ fmt.Printf("%s: ", label)
332332+ }
333333+ line, err := r.ReadString('\n')
334334+ if err != nil && err != io.EOF {
335335+ return def
336336+ }
337337+ line = strings.TrimRight(line, "\r\n")
338338+ if line == "" {
339339+ return def
340340+ }
341341+ return line
342342+}
343343+344344+func joinTagsOrNone(tags []string) string {
345345+ if len(tags) == 0 {
346346+ return "<none>"
347347+ }
348348+ return strings.Join(tags, ",")
349349+}
350350+351351+func splitTags(s string) []string {
352352+ parts := strings.Split(s, ",")
353353+ out := make([]string, 0, len(parts))
354354+ for _, p := range parts {
355355+ p = strings.TrimSpace(p)
356356+ if p != "" {
357357+ out = append(out, p)
358358+ }
359359+ }
360360+ return out
361361+}
362362+363363+func firstNonEmpty(vals ...string) string {
364364+ for _, v := range vals {
365365+ if v != "" {
366366+ return v
367367+ }
368368+ }
369369+ return ""
370370+}
371371+372372+func isTerminal(f *os.File) bool {
373373+ fi, err := f.Stat()
374374+ if err != nil {
375375+ return false
376376+ }
377377+ return (fi.Mode() & os.ModeCharDevice) != 0
93378}
+35-7
internal/cli/status.go
···66 "fmt"
77 "os"
88 "sort"
99+ "strings"
910 "text/tabwriter"
1011 "time"
1112···5455 agent := client.Agent()
55565657 start := time.Now()
5757- members, err := agent.Members()
5858+ nodes, err := agent.Members()
5859 apiRTT := time.Since(start)
5960 if err != nil {
6060- fmt.Printf("error fetching members from %s: %s\n", cfg.Address, err)
6161+ fmt.Printf("error fetching nodes from %s: %s\n", cfg.Address, err)
6162 os.Exit(1)
6263 }
6364···73747475 statusCounts := map[string]int{}
7576 tagKeyCounts := map[string]int{}
7676- for _, m := range members {
7777- statusCounts[m.StatusPretty()]++
7878- for k := range m.Tags {
7777+ for _, n := range nodes {
7878+ statusCounts[n.StatusPretty()]++
7979+ for k := range n.Tags {
7980 tagKeyCounts[k]++
8081 }
8182 }
82838383- total := len(members)
8484+ localServices, _ := agent.Services()
8585+ catalogServices, _ := client.Catalog().Services()
8686+8787+ total := len(nodes)
8488 alive := statusCounts["alive"]
8589 healthPct := 0.0
8690 if total > 0 {
···9498 fmt.Fprintf(tw, " agent\t%s\n", nodeName)
9599 fmt.Fprintf(tw, " api\t%s\n", cfg.Address)
96100 fmt.Fprintf(tw, " api rtt\t%s\n", apiRTT.Round(time.Microsecond))
9797- fmt.Fprintf(tw, " members\t%d\n", total)
101101+ fmt.Fprintf(tw, " nodes\t%d\n", total)
98102 fmt.Fprintf(tw, " health\t%.1f%% alive (%d/%d)\n", healthPct, alive, total)
99103 fmt.Fprintln(tw)
100104···106110 fmt.Fprintln(tw)
107111 }
108112113113+ fmt.Fprintln(tw, "services")
114114+ fmt.Fprintf(tw, " local agent\t%s\n", summarizeLocalServices(localServices))
115115+ fmt.Fprintf(tw, " cluster\t%s\n", summarizeCatalog(catalogServices))
116116+ fmt.Fprintln(tw)
117117+109118 fmt.Fprintln(tw, "status")
110119 for _, s := range sortedKeys(statusCounts) {
111120 fmt.Fprintf(tw, " %s\t%d\n", s, statusCounts[s])
···138147 sort.Strings(out)
139148 return out
140149}
150150+151151+func summarizeLocalServices(services map[string]*api.AgentService) string {
152152+ if len(services) == 0 {
153153+ return "none"
154154+ }
155155+ names := make([]string, 0, len(services))
156156+ for _, s := range services {
157157+ names = append(names, s.Service)
158158+ }
159159+ sort.Strings(names)
160160+ return fmt.Sprintf("%d (%s)", len(names), strings.Join(names, ", "))
161161+}
162162+163163+func summarizeCatalog(services map[string][]string) string {
164164+ if len(services) == 0 {
165165+ return "none"
166166+ }
167167+ return fmt.Sprintf("%d unique services", len(services))
168168+}
+14-10
main.go
···991010const usage = ` usage: cascade [command] [flags] [-h]
11111212- common commands:
1313- cascade agent start a cascade agent
1414- cascade members list cascade cluster members
1515- cascade services list services on a particular agent
1616- cascade status overview of the cascade cluster
1717-1818- advanced:
1919- cascade rtt estimate latency between nodes
1212+ commands:
1313+ cascade agent start a cascade agent
1414+ cascade status overview of the cascade cluster
1515+ cascade nodes list nodes
1616+ cascade services [name] list services
1717+ cascade register register a service to an agent
1818+ cascade deregister <id> remove a service from an agent
1919+ cascade rtt estimate latency between nodes
2020`
21212222// TODO: rename agent to something cooler
···3131 switch os.Args[1] {
3232 case "agent":
3333 cli.RunAgent(args)
3434- case "members":
3535- cli.RunMembers(args)
3434+ case "nodes":
3535+ cli.RunNodes(args)
3636 case "status":
3737 cli.RunStatus(args)
3838 case "services":
3939 cli.RunServices(args)
4040+ case "register":
4141+ cli.RunRegister(args)
4242+ case "deregister":
4343+ cli.RunDeregister(args)
4044 default:
4145 fmt.Fprintf(os.Stderr, "'%s' is not a valid command\n\n%s", command, usage)
4246 os.Exit(1)