mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-11 17:48:52 +00:00
fix(repository): idempotent sentinel agent creation via ON CONFLICT (M-6)
Sentinel agents (server-scanner, cloud-aws-sm, cloud-azure-kv, cloud-gcp-sm) were created on startup with a plain INSERT whose duplicate-key error was swallowed unconditionally. That silenced every other DB failure too (connectivity drop, permissions change, unrelated constraint violation) — a restart after the first boot quietly de-fanged cloud discovery and the network scanner (CWE-662, CWE-209- adjacent). Shape A: add AgentRepository.CreateIfNotExists using ON CONFLICT (id) DO NOTHING RETURNING id + sql.ErrNoRows discrimination. This keeps the strict Create semantics (duplicate-key is an error) intact for real agent registration and gives sentinels their own idempotent path. - repo: CreateIfNotExists returns (created bool, err error); false,nil on pre-existing row; false,wrapped err on anything else. - interface: CreateIfNotExists added to AgentRepository. - main.go: 4 sentinel sites log Error/Info/Debug distinctly. - mocks: service + integration mocks implement the new method. - tests: 4 new testcontainers integration tests cover first-insert, idempotent second-call, concurrent 16-goroutine race (exactly one creator, no duplicate-key panic), and pre-cancelled context surfacing. Coverage gates (go test -cover): service 67.6%/55, handler 78.6%/60, domain 92.7%/40, middleware 80.0%/30, crypto 86.7%/85. Race/vet/ golangci-lint v2.11.4 (0 issues)/govulncheck v1.2.0 clean across all touched packages.
This commit is contained in:
@@ -70,7 +70,9 @@ func (r *AgentRepository) Get(ctx context.Context, id string) (*domain.Agent, er
|
||||
return agent, nil
|
||||
}
|
||||
|
||||
// Create stores a new agent
|
||||
// Create stores a new agent. Duplicate-key errors surface to the caller —
|
||||
// real-agent registration paths rely on this to detect collisions. Use
|
||||
// CreateIfNotExists for sentinel/bootstrap paths where re-inserts are expected.
|
||||
func (r *AgentRepository) Create(ctx context.Context, agent *domain.Agent) error {
|
||||
if agent.ID == "" {
|
||||
agent.ID = uuid.New().String()
|
||||
@@ -92,6 +94,44 @@ func (r *AgentRepository) Create(ctx context.Context, agent *domain.Agent) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateIfNotExists creates an agent only if the ID doesn't already exist.
|
||||
// Used for sentinel agents (server-scanner, cloud-aws-sm, cloud-azure-kv,
|
||||
// cloud-gcp-sm) on first boot AND on every subsequent restart/upgrade — the
|
||||
// pre-M-6 code used plain INSERT, swallowed the duplicate-key error, and so
|
||||
// silently swallowed every other database failure too (CWE-662 /
|
||||
// CWE-209-adjacent). ON CONFLICT (id) DO NOTHING + RETURNING id +
|
||||
// sql.ErrNoRows distinguishes "row already existed" (created=false, err=nil)
|
||||
// from genuine errors (connectivity, permission, constraint violations
|
||||
// other than the id primary key) which still surface. Returns true if the
|
||||
// row was newly inserted, false if a row with the same ID already existed.
|
||||
func (r *AgentRepository) CreateIfNotExists(ctx context.Context, agent *domain.Agent) (bool, error) {
|
||||
if agent.ID == "" {
|
||||
agent.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
var id string
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
INSERT INTO agents (id, name, hostname, status, last_heartbeat_at, registered_at, api_key_hash,
|
||||
os, architecture, ip_address, version)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
RETURNING id
|
||||
`, agent.ID, agent.Name, agent.Hostname, agent.Status, agent.LastHeartbeatAt,
|
||||
agent.RegisteredAt, agent.APIKeyHash,
|
||||
agent.OS, agent.Architecture, agent.IPAddress, agent.Version).Scan(&id)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// ON CONFLICT DO NOTHING — a row with this ID already existed.
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("failed to create agent: %w", err)
|
||||
}
|
||||
|
||||
agent.ID = id
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Update modifies an existing agent
|
||||
func (r *AgentRepository) Update(ctx context.Context, agent *domain.Agent) error {
|
||||
result, err := r.db.ExecContext(ctx, `
|
||||
|
||||
Reference in New Issue
Block a user