Accept applied/reverted/failed in device status reports
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful

This commit is contained in:
2026-10-05 13:52:07 +11:00
parent 23357b953f
commit f2197c2189
6 changed files with 175 additions and 11 deletions
+21 -7
View File
@@ -7,6 +7,7 @@ import (
"encoding/json"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
@@ -279,11 +280,16 @@ func (s *Store) UpsertDevice(ctx context.Context, d model.Device) error {
})
}
// RecordDeviceStatus stores the generation an agent reports as applied.
func (s *Store) RecordDeviceStatus(ctx context.Context, name string, generation int64) error {
tag, err := s.pool.Exec(ctx,
`UPDATE devices SET reported_generation = $2, last_seen = now() WHERE name = $1`,
name, generation)
// RecordDeviceStatus stores an agent's apply report. reported_generation only
// advances on applied; every report updates the last-status columns.
func (s *Store) RecordDeviceStatus(ctx context.Context, name string, r model.StatusReport) error {
tag, err := s.pool.Exec(ctx, `
UPDATE devices SET
reported_generation = CASE WHEN $2 = 'applied' THEN $3 ELSE reported_generation END,
status = $2, status_generation = $3, status_error = $4,
status_at = now(), last_seen = now()
WHERE name = $1`,
name, string(r.Status), r.Generation, r.Error)
if err != nil {
return err
}
@@ -296,10 +302,14 @@ func (s *Store) RecordDeviceStatus(ctx context.Context, name string, generation
func (s *Store) GetDevice(ctx context.Context, name string) (model.Device, error) {
var d model.Device
var resolver, settings, reachable []byte
var st model.AgentStatus
var statusAt *time.Time
err := s.pool.QueryRow(ctx,
`SELECT name, class, COALESCE(fabric, ''), resolver, settings, reachable_prefixes
`SELECT name, class, COALESCE(fabric, ''), resolver, settings, reachable_prefixes,
reported_generation, status, status_generation, status_error, status_at
FROM devices WHERE name = $1`, name,
).Scan(&d.Name, &d.Class, &d.Fabric, &resolver, &settings, &reachable)
).Scan(&d.Name, &d.Class, &d.Fabric, &resolver, &settings, &reachable,
&st.AppliedGeneration, &st.Status, &st.Generation, &st.Error, &statusAt)
if errors.Is(err, pgx.ErrNoRows) {
return d, ErrNotFound
}
@@ -312,6 +322,10 @@ func (s *Store) GetDevice(ctx context.Context, name string) (model.Device, error
if err := json.Unmarshal(reachable, &d.ReachablePrefixes); err != nil {
return d, err
}
if statusAt != nil {
st.ReportedAt = *statusAt
d.AgentStatus = &st
}
return d, json.Unmarshal(settings, &d.Settings)
}
+54
View File
@@ -149,3 +149,57 @@ func TestNATTierRoundTrip(t *testing.T) {
t.Errorf("expected nat rows to cascade-delete with device, got %d", len(nats))
}
}
func TestRecordDeviceStatus(t *testing.T) {
s := newTestStore(t)
ctx := context.Background()
if err := s.UpsertDevice(ctx, model.Device{Name: "fw1", Class: model.ClassFirewall}); err != nil {
t.Fatalf("upsert device: %v", err)
}
if d, _ := s.GetDevice(ctx, "fw1"); d.AgentStatus != nil {
t.Fatalf("unreported device should have no agent_status: %+v", d.AgentStatus)
}
report := func(r model.StatusReport) model.AgentStatus {
t.Helper()
if err := r.Normalize(); err != nil {
t.Fatalf("normalize: %v", err)
}
if err := s.RecordDeviceStatus(ctx, "fw1", r); err != nil {
t.Fatalf("record %+v: %v", r, err)
}
d, err := s.GetDevice(ctx, "fw1")
if err != nil || d.AgentStatus == nil {
t.Fatalf("get device: %v %+v", err, d.AgentStatus)
}
return *d.AgentStatus
}
// Old agent payload: generation only.
got := report(model.StatusReport{Generation: 5})
if got.AppliedGeneration != 5 || got.Status != model.StatusApplied || got.Generation != 5 || got.ReportedAt.IsZero() {
t.Errorf("old payload: %+v", got)
}
got = report(model.StatusReport{Status: model.StatusApplied, Generation: 6})
if got.AppliedGeneration != 6 || got.Status != model.StatusApplied {
t.Errorf("applied: %+v", got)
}
got = report(model.StatusReport{Status: model.StatusReverted, Generation: 7, Error: "api unreachable"})
if got.AppliedGeneration != 6 || got.Status != model.StatusReverted || got.Generation != 7 || got.Error != "api unreachable" {
t.Errorf("reverted must not advance applied generation: %+v", got)
}
got = report(model.StatusReport{Status: model.StatusFailed, Generation: 8, Error: "nft: syntax error"})
if got.AppliedGeneration != 6 || got.Status != model.StatusFailed || got.Generation != 8 || got.Error != "nft: syntax error" {
t.Errorf("failed must not advance applied generation: %+v", got)
}
if err := s.RecordDeviceStatus(ctx, "fw1", model.StatusReport{Status: "bogus", Generation: 9}); err == nil {
t.Error("db should reject unknown status")
}
if err := s.RecordDeviceStatus(ctx, "nope", model.StatusReport{Status: model.StatusApplied}); err != store.ErrNotFound {
t.Errorf("unknown device: want ErrNotFound, got %v", err)
}
}