small pixel drawing of a pufferfish cascade

internal/agent/catalog_endpoint.go

package agent

import (
	"fmt"
	"net/http"
	"strings"

	"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) {
	out := s.agent.CatalogServices()
	if out == nil {
		out = map[string][]string{}
	}
	s.writeJSON(w, r, out)
}

// catalogService serves GET /v1/catalog/service/<name>. Returns one entry
// per known instance, joined with the owning node's serf membership info.
func (s *HTTPHandlers) catalogService(w http.ResponseWriter, r *http.Request) {
	name := strings.TrimPrefix(r.URL.Path, "/v1/catalog/service/")
	if name == "" || strings.Contains(name, "/") {
		http.Error(w, "service name required in path", http.StatusBadRequest)
		return
	}

	instances := s.agent.CatalogServiceInstances(name)
	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)
}

// catalogNodes serves GET /v1/catalog/nodes. Returns the serf membership
// list shaped as Consul-style Node entries.
func (s *HTTPHandlers) catalogNodes(w http.ResponseWriter, r *http.Request) {
	members := s.agent.CatalogNodes()
	out := make([]api.Node, 0, len(members))
	for _, m := range members {
		out = append(out, api.Node{
			Node:    m.Name,
			Address: m.Addr.String(),
			Meta:    m.Tags,
		})
	}
	s.writeJSON(w, r, out)
}

// catalogNode serves GET /v1/catalog/node/<name>. Returns the node and the
// services it owns.
func (s *HTTPHandlers) catalogNode(w http.ResponseWriter, r *http.Request) {
	name := strings.TrimPrefix(r.URL.Path, "/v1/catalog/node/")
	if name == "" || strings.Contains(name, "/") {
		http.Error(w, "node name required in path", http.StatusBadRequest)
		return
	}

	out := api.CatalogNode{
		Services: s.agent.CatalogNodeServices(name),
	}
	if out.Services == nil {
		out.Services = map[string]*api.AgentService{}
	}
	if m, ok := s.agent.MemberByName(name); ok {
		out.Node = &api.Node{
			Node:    m.Name,
			Address: m.Addr.String(),
			Meta:    m.Tags,
		}
	} else if len(out.Services) > 0 {
		// Node has gossipped services but serf hasn't surfaced it yet.
		out.Node = &api.Node{Node: name}
	} else {
		http.Error(w, fmt.Sprintf("unknown node %q", name), http.StatusNotFound)
		return
	}
	s.writeJSON(w, r, out)
}

// writeJSON is the standard 200 + application/json response helper.
func (s *HTTPHandlers) writeJSON(w http.ResponseWriter, r *http.Request, obj interface{}) {
	buf, err := s.marshalJSON(r, obj)
	if err != nil {
		s.agent.logger.Error("marshal response", err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Write(buf)
}