Accept applied/reverted/failed in device status reports
This commit is contained in:
+21
-7
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user