audit: add ETHOS time handling and updated_at NULL issues to tracking
This commit is contained in:
parent
0e26defa1d
commit
eac8a012bf
2 changed files with 17 additions and 148 deletions
|
|
@ -26,7 +26,23 @@ Found during the v0.2.0 version-string fix session (2026-05-21).
|
|||
**Issue:** Code references `created_at` but the migration never added the column. Causes `failed to initialize default security settings` at startup.
|
||||
**Impact:** Security settings fall back to hardcoded defaults instead of DB values.
|
||||
|
||||
## 5. `-dirty` version string root cause (resolved)
|
||||
## 5. ETHOS: Fragmented time handling — no universal clock
|
||||
**Files:** Multiple
|
||||
**Status:** Unresolved
|
||||
**Issue:** Time is set in at least 3 different ways across the codebase, violating ETHOS consistency:
|
||||
- Go `time.Now()` passed as query params (agents.go, agent_updates.go — caused the type mismatch bug #3)
|
||||
- SQL `NOW()` in some queries (agent_updates.go after fix)
|
||||
- Trigger `ON UPDATE` for `updated_at` on `agent_commands` (but no default on INSERT)
|
||||
- Manual `.UpdatedAt = time.Now()` assignment in `subsystems.go:52`
|
||||
**Impact:** Inconsistent timestamps, NULL scan errors, unclear source of truth for when things happened. Should be one canonical mechanism.
|
||||
|
||||
## 6. `agent_commands.updated_at` NULL on scan
|
||||
**File:** `server/internal/models/command.go:24`, `agent_commands` table
|
||||
**Status:** Partially fixed (model changed to `*time.Time`)
|
||||
**Issue:** `updated_at` column has no DEFAULT, trigger only fires on UPDATE. Old rows (or rows never updated) have NULL. Go `time.Time` can't scan NULL → `sql: Scan error on column index 17`.
|
||||
**Fix (partial):** Changed model to `*time.Time`. Still need DB migration to add DEFAULT and backfill.
|
||||
|
||||
## 7. `-dirty` version string root cause (resolved)
|
||||
**File:** `server/Dockerfile`
|
||||
**Status:** Fixed
|
||||
**Issue:** `.git/index` inside Docker build context referenced files outside the build context (partial checkout), causing `git describe --tags --dirty --always` to always return dirty.
|
||||
|
|
|
|||
|
|
@ -1,147 +0,0 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AgentCommand represents a command to be executed by an agent
|
||||
type AgentCommand struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
AgentID uuid.UUID `json:"agent_id" db:"agent_id"`
|
||||
CommandType string `json:"command_type" db:"command_type"`
|
||||
Params JSONB `json:"params" db:"params"`
|
||||
Status string `json:"status" db:"status"`
|
||||
Source string `json:"source" db:"source"`
|
||||
Signature string `json:"signature,omitempty" db:"signature"`
|
||||
KeyID string `json:"key_id,omitempty" db:"key_id"`
|
||||
SignedAt *time.Time `json:"signed_at,omitempty" db:"signed_at"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty" db:"expires_at"`
|
||||
IdempotencyKey *string `json:"idempotency_key,omitempty" db:"idempotency_key"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
SentAt *time.Time `json:"sent_at,omitempty" db:"sent_at"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" db:"completed_at"`
|
||||
Result JSONB `json:"result,omitempty" db:"result"`
|
||||
RetriedFromID *uuid.UUID `json:"retried_from_id,omitempty" db:"retried_from_id"`
|
||||
RetryCount int `json:"retry_count" db:"retry_count"`
|
||||
}
|
||||
|
||||
// Validate checks if the command has all required fields
|
||||
func (c *AgentCommand) Validate() error {
|
||||
if c.ID == uuid.Nil {
|
||||
return ErrCommandIDRequired
|
||||
}
|
||||
if c.AgentID == uuid.Nil {
|
||||
return ErrAgentIDRequired
|
||||
}
|
||||
if c.CommandType == "" {
|
||||
return ErrCommandTypeRequired
|
||||
}
|
||||
if c.Status == "" {
|
||||
return ErrStatusRequired
|
||||
}
|
||||
if c.Source != "manual" && c.Source != "system" {
|
||||
return ErrInvalidSource
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTerminal returns true if the command is in a terminal state
|
||||
func (c *AgentCommand) IsTerminal() bool {
|
||||
return c.Status == "completed" || c.Status == "failed" || c.Status == "cancelled"
|
||||
}
|
||||
|
||||
// CanRetry returns true if the command can be retried
|
||||
func (c *AgentCommand) CanRetry() bool {
|
||||
return c.Status == "failed" && c.RetriedFromID == nil
|
||||
}
|
||||
|
||||
// Predefined errors for validation
|
||||
var (
|
||||
ErrCommandIDRequired = errors.New("command ID cannot be zero UUID")
|
||||
ErrAgentIDRequired = errors.New("agent ID is required")
|
||||
ErrCommandTypeRequired = errors.New("command type is required")
|
||||
ErrStatusRequired = errors.New("status is required")
|
||||
ErrInvalidSource = errors.New("source must be 'manual' or 'system'")
|
||||
)
|
||||
|
||||
// CommandsResponse is returned when an agent checks in for commands
|
||||
type CommandsResponse struct {
|
||||
Commands []CommandItem `json:"commands"`
|
||||
RapidPolling *RapidPollingConfig `json:"rapid_polling,omitempty"`
|
||||
AcknowledgedIDs []string `json:"acknowledged_ids,omitempty"` // IDs server has received
|
||||
}
|
||||
|
||||
// RapidPollingConfig contains rapid polling configuration for the agent
|
||||
type RapidPollingConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Until string `json:"until"` // ISO 8601 timestamp
|
||||
}
|
||||
|
||||
// CommandItem represents a command in the response
|
||||
type CommandItem struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Params JSONB `json:"params"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
KeyID string `json:"key_id,omitempty"`
|
||||
SignedAt *time.Time `json:"signed_at,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
AgentID string `json:"agent_id,omitempty"`
|
||||
CreatedAt *time.Time `json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
// Command types
|
||||
const (
|
||||
CommandTypeCollectSpecs = "collect_specs"
|
||||
CommandTypeInstallUpdate = "install_updates"
|
||||
CommandTypeDryRunUpdate = "dry_run_update"
|
||||
CommandTypeConfirmDependencies = "confirm_dependencies"
|
||||
CommandTypeRollback = "rollback_update"
|
||||
CommandTypeUpdateAgent = "update_agent"
|
||||
CommandTypeEnableHeartbeat = "enable_heartbeat"
|
||||
CommandTypeDisableHeartbeat = "disable_heartbeat"
|
||||
CommandTypeReboot = "reboot"
|
||||
)
|
||||
|
||||
// Command statuses
|
||||
const (
|
||||
CommandStatusPending = "pending"
|
||||
CommandStatusSent = "sent"
|
||||
CommandStatusCompleted = "completed"
|
||||
CommandStatusFailed = "failed"
|
||||
CommandStatusTimedOut = "timed_out"
|
||||
CommandStatusCancelled = "cancelled"
|
||||
CommandStatusRunning = "running"
|
||||
)
|
||||
|
||||
// Command sources
|
||||
const (
|
||||
CommandSourceManual = "manual" // User-initiated via UI
|
||||
CommandSourceSystem = "system" // Auto-triggered by system operations
|
||||
)
|
||||
|
||||
// ActiveCommandInfo represents information about an active command for UI display
|
||||
type ActiveCommandInfo struct {
|
||||
ID uuid.UUID `json:"id" db:"id"`
|
||||
AgentID uuid.UUID `json:"agent_id" db:"agent_id"`
|
||||
CommandType string `json:"command_type" db:"command_type"`
|
||||
Params JSONB `json:"params" db:"params"`
|
||||
Status string `json:"status" db:"status"`
|
||||
Source string `json:"source" db:"source"`
|
||||
Signature string `json:"signature,omitempty" db:"signature"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
SentAt *time.Time `json:"sent_at,omitempty" db:"sent_at"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty" db:"completed_at"`
|
||||
Result JSONB `json:"result,omitempty" db:"result"`
|
||||
AgentHostname string `json:"agent_hostname" db:"agent_hostname"`
|
||||
PackageName string `json:"package_name" db:"package_name"`
|
||||
PackageType string `json:"package_type" db:"package_type"`
|
||||
RetriedFromID *uuid.UUID `json:"retried_from_id,omitempty" db:"retried_from_id"`
|
||||
IsRetry bool `json:"is_retry" db:"is_retry"`
|
||||
HasBeenRetried bool `json:"has_been_retried" db:"has_been_retried"`
|
||||
RetryCount int `json:"retry_count" db:"retry_count"`
|
||||
}
|
||||
Loading…
Reference in a new issue