mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
- Add persistent volume mounts for Go/npm caches (faster rebuilds) - Add shell config with helpful aliases and custom prompt - Add comprehensive devcontainer documentation - Add pre-commit hooks for Go formatting and linting - Use go-version-file in CI workflows instead of hardcoded versions - Simplify docker compose commands with --wait flag - Add gitignore entries for devcontainer auth files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
270 lines
7.6 KiB
Go
270 lines
7.6 KiB
Go
// Package updatedetection provides unified update detection across all Pulse-managed
|
|
// infrastructure types.
|
|
//
|
|
// The Manager coordinates update detection for Docker containers, receiving update status
|
|
// from Docker agents and checking registries on demand. It maintains an in-memory store
|
|
// of available updates and provides APIs for querying update status.
|
|
package updatedetection
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// Manager coordinates update detection across all infrastructure types.
|
|
type Manager struct {
|
|
store *Store
|
|
registry *RegistryChecker
|
|
logger zerolog.Logger
|
|
mu sync.RWMutex
|
|
|
|
// Configuration
|
|
enabled bool
|
|
checkInterval time.Duration
|
|
alertDelayHours int
|
|
enableDockerUpdates bool
|
|
}
|
|
|
|
// ManagerConfig holds configuration for the update detection manager.
|
|
type ManagerConfig struct {
|
|
Enabled bool // Master switch for update detection
|
|
CheckInterval time.Duration // How often to check for updates (default 6h)
|
|
AlertDelayHours int // Hours before alerting on a new update (default 24)
|
|
EnableDockerUpdates bool // Enable Docker image update detection
|
|
}
|
|
|
|
// DefaultManagerConfig returns sensible default configuration.
|
|
func DefaultManagerConfig() ManagerConfig {
|
|
return ManagerConfig{
|
|
Enabled: true,
|
|
CheckInterval: 6 * time.Hour,
|
|
AlertDelayHours: 24,
|
|
EnableDockerUpdates: true,
|
|
}
|
|
}
|
|
|
|
// NewManager creates a new update detection manager.
|
|
func NewManager(cfg ManagerConfig, logger zerolog.Logger) *Manager {
|
|
return &Manager{
|
|
store: NewStore(),
|
|
registry: NewRegistryChecker(logger),
|
|
logger: logger.With().Str("component", "updatedetection").Logger(),
|
|
enabled: cfg.Enabled,
|
|
checkInterval: cfg.CheckInterval,
|
|
alertDelayHours: cfg.AlertDelayHours,
|
|
enableDockerUpdates: cfg.EnableDockerUpdates,
|
|
}
|
|
}
|
|
|
|
// ProcessDockerContainerUpdate processes an update status report from a Docker agent.
|
|
// This is called when the agent reports container data with update status.
|
|
func (m *Manager) ProcessDockerContainerUpdate(
|
|
hostID string,
|
|
containerID string,
|
|
containerName string,
|
|
image string,
|
|
currentDigest string,
|
|
updateStatus *ContainerUpdateStatus,
|
|
) {
|
|
if !m.enabled || !m.enableDockerUpdates {
|
|
return
|
|
}
|
|
|
|
if updateStatus == nil {
|
|
// No update status provided by agent - we can still track the container
|
|
// but won't know about updates yet
|
|
return
|
|
}
|
|
|
|
if !updateStatus.UpdateAvailable {
|
|
// No update available - remove any existing update entry
|
|
m.store.DeleteUpdatesForResource(containerID)
|
|
return
|
|
}
|
|
|
|
// Create or update the update entry
|
|
updateID := "docker:" + hostID + ":" + containerID
|
|
update := &UpdateInfo{
|
|
ID: updateID,
|
|
ResourceID: containerID,
|
|
ResourceType: "docker",
|
|
ResourceName: containerName,
|
|
HostID: hostID,
|
|
Type: UpdateTypeDockerImage,
|
|
CurrentDigest: updateStatus.CurrentDigest,
|
|
LatestDigest: updateStatus.LatestDigest,
|
|
LastChecked: updateStatus.LastChecked,
|
|
CurrentVersion: image,
|
|
}
|
|
|
|
if updateStatus.Error != "" {
|
|
update.Error = updateStatus.Error
|
|
}
|
|
|
|
m.store.UpsertUpdate(update)
|
|
|
|
m.logger.Debug().
|
|
Str("container", containerName).
|
|
Str("image", image).
|
|
Str("hostID", hostID).
|
|
Bool("hasUpdate", updateStatus.UpdateAvailable).
|
|
Msg("Processed container update status")
|
|
}
|
|
|
|
// CheckImageUpdate checks a specific image for updates using the registry API.
|
|
// This can be called on demand from the server side.
|
|
func (m *Manager) CheckImageUpdate(ctx context.Context, image, currentDigest string) (*ImageUpdateInfo, error) {
|
|
if !m.enabled {
|
|
return nil, nil
|
|
}
|
|
|
|
return m.registry.CheckImageUpdate(ctx, image, currentDigest)
|
|
}
|
|
|
|
// GetUpdates returns all tracked updates, optionally filtered.
|
|
func (m *Manager) GetUpdates(filters UpdateFilters) []*UpdateInfo {
|
|
all := m.store.GetAllUpdates()
|
|
|
|
if filters.IsEmpty() {
|
|
return all
|
|
}
|
|
|
|
result := make([]*UpdateInfo, 0)
|
|
for _, update := range all {
|
|
if filters.Matches(update) {
|
|
result = append(result, update)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// GetUpdatesForHost returns all updates for a specific host.
|
|
func (m *Manager) GetUpdatesForHost(hostID string) []*UpdateInfo {
|
|
return m.store.GetUpdatesForHost(hostID)
|
|
}
|
|
|
|
// GetUpdatesForResource returns the update for a specific resource.
|
|
func (m *Manager) GetUpdatesForResource(resourceID string) *UpdateInfo {
|
|
return m.store.GetUpdatesForResource(resourceID)
|
|
}
|
|
|
|
// GetSummary returns aggregated update statistics by host.
|
|
func (m *Manager) GetSummary() map[string]*UpdateSummary {
|
|
return m.store.GetSummary()
|
|
}
|
|
|
|
// GetTotalCount returns the total number of tracked updates.
|
|
func (m *Manager) GetTotalCount() int {
|
|
return m.store.Count()
|
|
}
|
|
|
|
// DeleteUpdatesForHost removes all updates for a host (called when host is removed).
|
|
func (m *Manager) DeleteUpdatesForHost(hostID string) {
|
|
m.store.DeleteUpdatesForHost(hostID)
|
|
}
|
|
|
|
// AddRegistryConfig adds registry authentication configuration.
|
|
func (m *Manager) AddRegistryConfig(cfg RegistryConfig) {
|
|
m.registry.AddRegistryConfig(cfg)
|
|
}
|
|
|
|
// CleanupStale removes update entries that haven't been refreshed recently.
|
|
// This is called periodically to clean up stale entries from removed containers.
|
|
func (m *Manager) CleanupStale(maxAge time.Duration) int {
|
|
all := m.store.GetAllUpdates()
|
|
cutoff := time.Now().Add(-maxAge)
|
|
removed := 0
|
|
|
|
for _, update := range all {
|
|
if update.LastChecked.Before(cutoff) {
|
|
m.store.DeleteUpdate(update.ID)
|
|
removed++
|
|
}
|
|
}
|
|
|
|
if removed > 0 {
|
|
m.logger.Info().Int("removed", removed).Msg("Cleaned up stale update entries")
|
|
}
|
|
|
|
return removed
|
|
}
|
|
|
|
// UpdateFilters allows filtering update queries.
|
|
type UpdateFilters struct {
|
|
HostID string // Filter by host
|
|
ResourceType string // Filter by resource type (docker, lxc, vm, etc)
|
|
UpdateType UpdateType // Filter by update type
|
|
Severity UpdateSeverity
|
|
HasError *bool // Filter by error status
|
|
}
|
|
|
|
// IsEmpty returns true if no filters are set.
|
|
func (f *UpdateFilters) IsEmpty() bool {
|
|
return f.HostID == "" && f.ResourceType == "" && f.UpdateType == "" && f.Severity == "" && f.HasError == nil
|
|
}
|
|
|
|
// Matches returns true if the update matches all set filters.
|
|
func (f *UpdateFilters) Matches(update *UpdateInfo) bool {
|
|
if f.HostID != "" && update.HostID != f.HostID {
|
|
return false
|
|
}
|
|
if f.ResourceType != "" && update.ResourceType != f.ResourceType {
|
|
return false
|
|
}
|
|
if f.UpdateType != "" && update.Type != f.UpdateType {
|
|
return false
|
|
}
|
|
if f.Severity != "" && update.Severity != f.Severity {
|
|
return false
|
|
}
|
|
if f.HasError != nil {
|
|
hasError := update.Error != ""
|
|
if hasError != *f.HasError {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Enabled returns whether update detection is enabled.
|
|
func (m *Manager) Enabled() bool {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return m.enabled
|
|
}
|
|
|
|
// SetEnabled enables or disables update detection.
|
|
func (m *Manager) SetEnabled(enabled bool) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.enabled = enabled
|
|
}
|
|
|
|
// AlertDelayHours returns the configured delay before alerting on updates.
|
|
func (m *Manager) AlertDelayHours() int {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return m.alertDelayHours
|
|
}
|
|
|
|
// GetUpdatesReadyForAlert returns updates that have been pending for longer than the alert delay.
|
|
func (m *Manager) GetUpdatesReadyForAlert() []*UpdateInfo {
|
|
m.mu.RLock()
|
|
delay := time.Duration(m.alertDelayHours) * time.Hour
|
|
m.mu.RUnlock()
|
|
|
|
all := m.store.GetAllUpdates()
|
|
cutoff := time.Now().Add(-delay)
|
|
ready := make([]*UpdateInfo, 0)
|
|
|
|
for _, update := range all {
|
|
if update.FirstDetected.Before(cutoff) && update.Error == "" {
|
|
ready = append(ready, update)
|
|
}
|
|
}
|
|
|
|
return ready
|
|
}
|