This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
// Package agentstore is the transport boundary between pulse-agent and pulse-api.
|
||||
//
|
||||
// The agent runs with the narrow read-only host access it needs and never exposes a
|
||||
// network endpoint; the API never reaches out to the host. Both processes already share
|
||||
// PostgreSQL on the internal-only Compose network (see deploy/compose.yaml and ADR-0010),
|
||||
// so the agent writes bounded, normalized snapshots into the database and the API reads
|
||||
// the most recent one per capability. This keeps the privilege separation required by
|
||||
// SYSTEM_ARCHITECTURE section "pulse-agent" without adding an inbound port to the agent.
|
||||
//
|
||||
// Freshness is deliberately the reader's problem, not the writer's: a snapshot carries
|
||||
// the time the agent observed it, and the reader decides whether that is still usable.
|
||||
// A capability with no snapshot, or one older than its freshness window, resolves to
|
||||
// Unknown and never to Healthy (ADR-0008).
|
||||
package agentstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Capability identifies one bounded telemetry surface an agent can report. The set is
|
||||
// closed: a reader must never accept a capability it does not recognize, because an
|
||||
// unknown capability cannot be normalized or bounded.
|
||||
type Capability string
|
||||
|
||||
const (
|
||||
CapabilityHost Capability = "host"
|
||||
CapabilityProcesses Capability = "processes"
|
||||
CapabilityContainers Capability = "containers"
|
||||
CapabilityArray Capability = "array"
|
||||
CapabilityDisks Capability = "disks"
|
||||
CapabilityPools Capability = "pools"
|
||||
CapabilityShares Capability = "shares"
|
||||
)
|
||||
|
||||
// Capabilities lists every capability the platform recognizes, in a stable order.
|
||||
func Capabilities() []Capability {
|
||||
return []Capability{
|
||||
CapabilityHost, CapabilityProcesses, CapabilityContainers,
|
||||
CapabilityArray, CapabilityDisks, CapabilityPools, CapabilityShares,
|
||||
}
|
||||
}
|
||||
|
||||
// Valid reports whether the capability is one this platform recognizes.
|
||||
func (c Capability) Valid() bool {
|
||||
for _, known := range Capabilities() {
|
||||
if c == known {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MaxPayloadBytes bounds a single snapshot. The largest realistic payload is the process
|
||||
// inventory at the documented scale target; this leaves generous headroom while keeping a
|
||||
// misbehaving or compromised agent from filling the database.
|
||||
const MaxPayloadBytes = 2 << 20
|
||||
|
||||
// ErrNoSnapshot is returned by Reader.Latest when the capability has never been reported.
|
||||
// It is an expected condition on a fresh install, not a failure: callers translate it into
|
||||
// an Unknown status with an explicit reason.
|
||||
var ErrNoSnapshot = errors.New("no agent snapshot recorded")
|
||||
|
||||
// Snapshot is one bounded observation of a single capability.
|
||||
type Snapshot struct {
|
||||
// AgentID identifies the reporting agent.
|
||||
AgentID string
|
||||
// Capability is the telemetry surface this payload describes.
|
||||
Capability Capability
|
||||
// ObservedAt is when the agent read the underlying source, in UTC.
|
||||
ObservedAt time.Time
|
||||
// ReceivedAt is when the store accepted the snapshot, in UTC. It is set by the
|
||||
// writer implementation, never by the agent, so a skewed agent clock cannot make
|
||||
// stale data look fresh.
|
||||
ReceivedAt time.Time
|
||||
// Payload is the domain RawSnapshot for this capability, JSON encoded.
|
||||
Payload json.RawMessage
|
||||
}
|
||||
|
||||
// Age reports how long ago the agent observed this snapshot.
|
||||
func (s Snapshot) Age(now time.Time) time.Duration { return now.Sub(s.ObservedAt) }
|
||||
|
||||
// Writer is the narrow interface pulse-agent depends on. The agent must not be able to
|
||||
// read other agents' data or mutate anything else.
|
||||
type Writer interface {
|
||||
// Put records the newest snapshot for one capability, replacing any previous one.
|
||||
// Implementations reject an unknown capability, an oversized payload, a zero or
|
||||
// future ObservedAt, and payloads that are not valid JSON objects.
|
||||
Put(ctx context.Context, snapshot Snapshot) error
|
||||
}
|
||||
|
||||
// Reader is the narrow interface pulse-api depends on.
|
||||
type Reader interface {
|
||||
// Latest returns the most recent snapshot for the capability, or ErrNoSnapshot.
|
||||
Latest(ctx context.Context, capability Capability) (Snapshot, error)
|
||||
}
|
||||
|
||||
// Store is the combined boundary. Only the migration-owning implementation satisfies it.
|
||||
type Store interface {
|
||||
Writer
|
||||
Reader
|
||||
}
|
||||
Reference in New Issue
Block a user