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.
|
**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.
|
**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`
|
**File:** `server/Dockerfile`
|
||||||
**Status:** Fixed
|
**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.
|
**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