fluid-core/internal/control/api.go

96 lines
2.8 KiB
Go
Raw Normal View History

// Package control implements the FLUID control-plane APIs.
//
// ArchitectureBlueprint.md section 44 asks for a small set of internal control
// APIs. They are internal on purpose: this is the surface that publishes
// revisions and records governance decisions, and it must never be reachable by
// the consumers whose behaviour it evolves in response to.
//
// Nothing here is on the request path. The data plane keeps serving when this
// server is down (invariant 2), which is also why the CLI reads the evidence
// store directly rather than through these endpoints.
package control
import (
"encoding/json"
"errors"
"net/http"
"strings"
"github.com/tegwick/fluid-core/internal/evidence"
)
// Server exposes the control APIs over HTTP.
type Server struct {
revisions *RevisionAPI
intents *IntentAPI
}
// NewServer wires the control APIs.
func NewServer(rev *RevisionAPI, in *IntentAPI) *Server {
return &Server{revisions: rev, intents: in}
}
// Routes returns the control-plane mux.
func (s *Server) Routes() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/control/v1/revisions", s.revisions.handleCollection)
mux.HandleFunc("/control/v1/revisions/", s.revisions.handleItem)
mux.HandleFunc("/control/v1/intents", s.intents.handleCollection)
mux.HandleFunc("/control/v1/intents/", s.intents.handleItem)
mux.HandleFunc("/control/v1/intents/active", s.intents.handleActive)
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
})
return mux
}
// apiError is the control-plane error shape.
type apiError struct {
Error string `json:"error"`
Detail string `json:"detail,omitempty"`
Causes []string `json:"causes,omitempty"`
}
func writeJSON(w http.ResponseWriter, status int, body any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(body)
}
func writeError(w http.ResponseWriter, status int, msg string, causes ...string) {
writeJSON(w, status, apiError{Error: msg, Causes: causes})
}
// statusForStoreError maps store failures onto HTTP without leaking detail.
func statusForStoreError(err error) int {
if errors.Is(err, evidence.ErrNotFound) {
return http.StatusNotFound
}
return http.StatusInternalServerError
}
// pathTail returns the segment after prefix, or "" when there is none.
func pathTail(path, prefix string) string {
rest := strings.TrimPrefix(path, prefix)
rest = strings.Trim(rest, "/")
if rest == "" {
return ""
}
if i := strings.Index(rest, "/"); i >= 0 {
return rest[:i]
}
return rest
}
func decodeBody(r *http.Request, into any) error {
dec := json.NewDecoder(http.MaxBytesReader(nil, r.Body, 1<<20))
dec.DisallowUnknownFields()
if err := dec.Decode(into); err != nil {
return err
}
return nil
}