eeb44057db
The paths keyed on one certname took the pass-through, so a node both
backends hold answered from whichever was configured first while /facts
answered from whichever held its newer report.
- add a route claiming /pdb/query/v4/{nodes,factsets,catalogs}/<certname>
- order the backends for it by the freshness map the /facts merge uses
- try the remaining backends after the owner, replaying upstream's 404 when none holds the certname
- assert ownership, the upstream 404 body, a failed backend and query forwarding against captured openvoxdb shapes
82 lines
2.9 KiB
Go
82 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
factsetsPath = "/pdb/query/v4/factsets"
|
|
catalogsPath = "/pdb/query/v4/catalogs"
|
|
|
|
certnameRouteName = "/pdb/query/v4/{nodes,factsets,catalogs}/<certname>"
|
|
)
|
|
|
|
// certnamePrefixes are the endpoints whose next path segment is a certname.
|
|
// openvoxdb resolves that segment to a parent row before it serves anything
|
|
// under it — status-not-found-response for the singleton
|
|
// (src/puppetlabs/puppetdb/http/handlers.clj:98-120,252-253,342-343,372 and
|
|
// http.clj:238-242) and parent-check for every child path
|
|
// (middleware.clj:381-398, mounted at handlers.clj:345-347,255-262,373-380) —
|
|
// so each of these paths describes exactly one node's data and 404s when the
|
|
// backend holds none of it.
|
|
var certnamePrefixes = []string{nodesPath + "/", factsetsPath + "/", catalogsPath + "/"}
|
|
|
|
// certnameFor returns the certname a path is keyed on, empty when the path is
|
|
// not one of these routes.
|
|
func certnameFor(path string) string {
|
|
for _, prefix := range certnamePrefixes {
|
|
rest, ok := strings.CutPrefix(path, prefix)
|
|
if !ok {
|
|
continue
|
|
}
|
|
certname, _, _ := strings.Cut(rest, "/")
|
|
return certname
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func isCertnameRoute(path string) bool { return certnameFor(path) != "" }
|
|
|
|
// serveByOwner answers a path keyed on one certname from the backend that owns
|
|
// that certname, so a node resolves here exactly as it resolves in the merged
|
|
// collections. Without it the answer is whichever backend is configured first,
|
|
// and a node both backends hold reports one backend's facts through /facts and
|
|
// the other's through /nodes/<certname>/facts.
|
|
//
|
|
// The owner's reply is passed through whole rather than merged: openvoxdb
|
|
// answers these paths with a bare object or a parent-check 404, neither of
|
|
// which is the record array the merges are built on. The remaining backends are
|
|
// tried after it, so a node only one backend holds is still served and a
|
|
// certname no backend holds still answers with openvoxdb's own 404 body.
|
|
func (s *Server) serveByOwner(w http.ResponseWriter, r *http.Request) {
|
|
s.proxyOrdered(w, r, s.ownerFirst(r.Context(), certnameFor(r.URL.Path)))
|
|
}
|
|
|
|
// ownerFirst puts the backend holding a certname's newest report ahead of the
|
|
// rest, from the same freshness map the /facts merge attributes records with.
|
|
// Static merge has no per-certname owner, and neither has a certname absent
|
|
// from the map, so both keep configured order — the tie-break the merges use.
|
|
func (s *Server) ownerFirst(ctx context.Context, certname string) []Backend {
|
|
if s.cfg.Merge == mergeStatic || certname == "" {
|
|
return s.cfg.Backends
|
|
}
|
|
owner := s.freshnessMap(ctx, nil)[certname]
|
|
if owner == "" {
|
|
return s.cfg.Backends
|
|
}
|
|
ordered := make([]Backend, 0, len(s.cfg.Backends))
|
|
for _, b := range s.cfg.Backends {
|
|
if b.Name == owner {
|
|
ordered = append(ordered, b)
|
|
}
|
|
}
|
|
for _, b := range s.cfg.Backends {
|
|
if b.Name != owner {
|
|
ordered = append(ordered, b)
|
|
}
|
|
}
|
|
return ordered
|
|
}
|