small pixel drawing of a pufferfish cascade

moar ux
Jes Olson j3s@c3f.net
Sat, 23 May 2026 14:20:14 -0500
commit

f5e2cac89c04823f4bd241781c0542df0bbba97c

parent

a46afb7f2b02bebf0e167c30448f6b258b9cab32

M api/catalog.goapi/catalog.go

@@ -70,6 +70,23 @@ }

return out, nil } +// Instances returns every gossipped service instance across the cluster. +// One round trip — useful when you want to render a per-node view rather +// than per-service. +func (cat *Catalog) Instances() ([]*CatalogService, error) { + r := cat.c.newRequest("GET", "/v1/catalog/instances") + _, resp, err := requireOK(cat.c.doRequest(r)) + if err != nil { + return nil, err + } + defer closeResponseBody(resp) + var out []*CatalogService + if err := decodeJSONBody(resp.Body, &out); err != nil { + return nil, err + } + return out, nil +} + // Nodes returns every node serf knows about. func (cat *Catalog) Nodes() ([]*Node, error) { r := cat.c.newRequest("GET", "/v1/catalog/nodes")
M internal/agent/agent.gointernal/agent/agent.go

@@ -245,6 +245,12 @@ func (a *Agent) CatalogServiceInstances(name string) []NodeService {

return a.services.ServiceInstances(name) } +// CatalogAllInstances returns every gossipped service instance across the +// whole cluster as a flat slice. +func (a *Agent) CatalogAllInstances() []NodeService { + return a.services.AllInstances() +} + // CatalogNodeServices returns the services owned by the named node. func (a *Agent) CatalogNodeServices(node string) map[string]*api.AgentService { return a.services.NodeServices(node)
M internal/agent/catalog_endpoint.gointernal/agent/catalog_endpoint.go

@@ -8,6 +8,29 @@

"j3s.sh/cascade/api" ) +// catalogInstances serves GET /v1/catalog/instances. Returns one entry +// per (node, service) pair across the whole cluster. +func (s *HTTPHandlers) catalogInstances(w http.ResponseWriter, r *http.Request) { + instances := s.agent.CatalogAllInstances() + out := make([]api.CatalogService, 0, len(instances)) + for _, inst := range instances { + entry := api.CatalogService{ + Node: inst.Node, + ServiceID: inst.Service.ID, + ServiceName: inst.Service.Service, + ServiceAddress: inst.Service.Address, + ServicePort: inst.Service.Port, + ServiceTags: inst.Service.Tags, + ServiceMeta: inst.Service.Meta, + } + if m, ok := s.agent.MemberByName(inst.Node); ok { + entry.Address = m.Addr.String() + } + out = append(out, entry) + } + s.writeJSON(w, r, out) +} + // catalogServices serves GET /v1/catalog/services. Returns the cluster-wide // map of service name -> union of tags seen on any instance. func (s *HTTPHandlers) catalogServices(w http.ResponseWriter, r *http.Request) {
M internal/agent/http.gointernal/agent/http.go

@@ -117,6 +117,7 @@ "/v1/agent/service/register": s.agentServiceRegister,

"/v1/agent/service/deregister/": s.agentServiceDeregister, "/v1/catalog/services": s.catalogServices, "/v1/catalog/service/": s.catalogService, + "/v1/catalog/instances": s.catalogInstances, "/v1/catalog/nodes": s.catalogNodes, "/v1/catalog/node/": s.catalogNode, }
M internal/agent/services.gointernal/agent/services.go

@@ -212,6 +212,27 @@ }

return out } +// AllInstances returns every gossipped service instance across the entire +// cluster, one entry per (node, service) pair. +func (s *ServiceStore) AllInstances() []NodeService { + s.mu.RLock() + defer s.mu.RUnlock() + var out []NodeService + for node, byID := range s.services { + for _, svc := range byID { + cp := *svc + out = append(out, NodeService{Node: node, Service: &cp}) + } + } + sort.Slice(out, func(i, j int) bool { + if out[i].Node != out[j].Node { + return out[i].Node < out[j].Node + } + return out[i].Service.ID < out[j].Service.ID + }) + return out +} + // ServiceInstances returns every gossipped instance of the named service, // across all nodes. func (s *ServiceStore) ServiceInstances(name string) []NodeService {
M internal/cli/nodes.gointernal/cli/nodes.go

@@ -5,6 +5,7 @@ "bytes"

"flag" "fmt" "os" + "sort" "strings" "text/tabwriter"

@@ -13,7 +14,6 @@ )

type nodesCommand struct { flagAPIAddr string - flagDetails bool } func (c nodesCommand) Usage() {

@@ -30,8 +30,6 @@ func (c *nodesCommand) 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)

@@ -57,15 +55,25 @@ fmt.Println(err)

os.Exit(1) } + // Fetch the full instance list once; group by owning node. + instances, _ := client.Catalog().Instances() + servicesByNode := map[string][]string{} + for _, inst := range instances { + servicesByNode[inst.Node] = append(servicesByNode[inst.Node], inst.ServiceName) + } + for n := range servicesByNode { + sort.Strings(servicesByNode[n]) + } + var b bytes.Buffer tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0) - fmt.Fprintf(tw, "node\taddr\tstatus\ttags\n") + fmt.Fprintf(tw, "node\taddr\tstatus\tservices\n") for _, n := range nodes { - fmt.Fprintf(tw, "%s\t", n.Name) - fmt.Fprintf(tw, "%s:%d\t", n.Addr, n.Port) - fmt.Fprintf(tw, "%s\t", n.StatusPretty()) - fmt.Fprintf(tw, "%s\t", c.printTags(n.Tags)) - fmt.Fprintln(tw) + services := "<none>" + if names := servicesByNode[n.Name]; len(names) > 0 { + services = strings.Join(names, ", ") + } + fmt.Fprintf(tw, "%s\t%s:%d\t%s\t%s\n", n.Name, n.Addr, n.Port, n.StatusPretty(), services) } if err := tw.Flush(); err != nil { fmt.Printf("error flushing tabwriter: %s", err)

@@ -73,14 +81,3 @@ os.Exit(1)

} fmt.Print(b.String()) } - -func (c nodesCommand) printTags(tags map[string]string) string { - var results []string - for k, v := range tags { - results = append(results, fmt.Sprintf("%s=%s", k, v)) - } - if len(results) == 0 { - return "<none>" - } - return strings.Join(results, ",") -}
M internal/cli/status.gointernal/cli/status.go

@@ -74,12 +74,8 @@ dnsDomain = self.Config.DNSDomain

} statusCounts := map[string]int{} - tagKeyCounts := map[string]int{} for _, n := range nodes { statusCounts[n.StatusPretty()]++ - for k := range n.Tags { - tagKeyCounts[k]++ - } } localServices, _ := agent.Services()

@@ -122,14 +118,6 @@

fmt.Fprintln(tw, "status") for _, s := range sortedKeys(statusCounts) { fmt.Fprintf(tw, " %s\t%d\n", s, statusCounts[s]) - } - - if len(tagKeyCounts) > 0 { - fmt.Fprintln(tw) - fmt.Fprintln(tw, "tags") - for _, k := range sortedKeys(tagKeyCounts) { - fmt.Fprintf(tw, " %s\t%d nodes\n", k, tagKeyCounts[k]) - } } if err := tw.Flush(); err != nil {