mirror of
https://github.com/replicatedhq/ttl.sh.git
synced 2026-08-19 01:36:14 +00:00
105 lines
3.1 KiB
Go
105 lines
3.1 KiB
Go
// Package server implements the HTTP surface for the TTL service: a CloudEvents
|
|
// sink for zot image events and a health check.
|
|
package server
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/events"
|
|
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/ttl"
|
|
)
|
|
|
|
// Store is the subset of the persistence layer the server needs.
|
|
// Implementations must be safe for concurrent use: the HTTP handlers call it
|
|
// from whatever goroutine net/http hands them, without serializing first.
|
|
type Store interface {
|
|
Upsert(repo, tag, digest string, expiresAt, createdAt time.Time) error
|
|
}
|
|
|
|
// Server is the HTTP server: events sink + healthz. It holds no lock of its
|
|
// own; concurrent event POSTs are safe by way of the Store contract above.
|
|
type Server struct {
|
|
store Store
|
|
defaultTTL time.Duration
|
|
maxTTL time.Duration
|
|
clock func() time.Time
|
|
}
|
|
|
|
// New constructs a Server. defaultTTL and maxTTL drive the expiry policy applied
|
|
// to incoming events.
|
|
func New(st Store, defaultTTL, maxTTL time.Duration, clock func() time.Time) *Server {
|
|
return &Server{
|
|
store: st,
|
|
defaultTTL: defaultTTL,
|
|
maxTTL: maxTTL,
|
|
clock: clock,
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
w.Header().Set("Allow", http.MethodPost)
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
|
|
if err != nil {
|
|
http.Error(w, "read body: "+err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
evt, err := events.Parse(r, body)
|
|
if err != nil {
|
|
log.Printf("events: parse error: %v", err)
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if evt.Type != events.ImageUpdatedType {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
if evt.Data.Name == "" || evt.Data.Reference == "" {
|
|
log.Printf("events: image.updated missing name/reference; ignoring (digest=%q)", evt.Data.Digest)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
now := s.clock()
|
|
expires := ttl.ComputeExpiry(evt.Data.Reference, now, s.defaultTTL, s.maxTTL)
|
|
|
|
err = s.store.Upsert(evt.Data.Name, evt.Data.Reference, evt.Data.Digest, expires, now)
|
|
if err != nil {
|
|
log.Printf("events: upsert %s:%s: %v", evt.Data.Name, evt.Data.Reference, err)
|
|
http.Error(w, "store error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
log.Printf("events: recorded %s:%s digest=%s expires_at=%s",
|
|
evt.Data.Name, evt.Data.Reference, shortDigest(evt.Data.Digest), expires.UTC().Format(time.RFC3339))
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = io.WriteString(w, "ok\n")
|
|
}
|
|
|
|
// Routes returns the HTTP handler wiring the server's endpoints.
|
|
func (s *Server) Routes() http.Handler {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/events", s.handleEvents)
|
|
mux.HandleFunc("/healthz", s.handleHealthz)
|
|
return mux
|
|
}
|
|
|
|
func shortDigest(d string) string {
|
|
if len(d) > 19 { // "sha256:" + 12 hex chars
|
|
return d[:19]
|
|
}
|
|
return d
|
|
}
|