mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Harden update history persistence root
This commit is contained in:
parent
b398c9c405
commit
6b45fd0502
3 changed files with 72 additions and 12 deletions
|
|
@ -156,6 +156,10 @@ manager: when `internal/updates/manager.go` enumerates already-owned extract,
|
|||
temp, backup, or restore directories, it must rejoin discovered entry names
|
||||
through the shared storage-path helper instead of rebuilding raw
|
||||
`filepath.Join(dir, entry.Name())` paths.
|
||||
That same storage boundary also governs update-history persistence:
|
||||
`internal/updates/history.go` must normalize its owned data directory and
|
||||
resolve the fixed `update-history.jsonl` leaf through the shared storage-path
|
||||
helper instead of joining raw caller-provided directory strings.
|
||||
That same boundary also governs install.sh rollback restore targets:
|
||||
`adapter_installsh.go` may not hardcode `/etc/pulse` for rollback safety
|
||||
backups or config restore, and must derive the rollback config directory
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/securityutil"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
|
|
@ -19,11 +20,13 @@ import (
|
|||
type UpdateStatusType string
|
||||
|
||||
const (
|
||||
StatusInProgress UpdateStatusType = "in_progress"
|
||||
StatusSuccess UpdateStatusType = "success"
|
||||
StatusFailed UpdateStatusType = "failed"
|
||||
StatusRolledBack UpdateStatusType = "rolled_back"
|
||||
StatusCancelled UpdateStatusType = "cancelled"
|
||||
StatusInProgress UpdateStatusType = "in_progress"
|
||||
StatusSuccess UpdateStatusType = "success"
|
||||
StatusFailed UpdateStatusType = "failed"
|
||||
StatusRolledBack UpdateStatusType = "rolled_back"
|
||||
StatusCancelled UpdateStatusType = "cancelled"
|
||||
defaultUpdateHistoryDataDir = "/var/lib/pulse"
|
||||
updateHistoryFileName = "update-history.jsonl"
|
||||
)
|
||||
|
||||
// InitiatedBy represents who triggered the update
|
||||
|
|
@ -90,19 +93,37 @@ type UpdateHistory struct {
|
|||
maxCache int
|
||||
}
|
||||
|
||||
func resolveUpdateHistoryLogPath(dataDir string) (string, string, error) {
|
||||
normalizedDir := strings.TrimSpace(dataDir)
|
||||
if normalizedDir == "" {
|
||||
normalizedDir = defaultUpdateHistoryDataDir
|
||||
}
|
||||
|
||||
normalizedDir, err := securityutil.NormalizeStorageDir(normalizedDir)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("normalize update history directory: %w", err)
|
||||
}
|
||||
|
||||
logPath, err := securityutil.JoinStorageLeaf(normalizedDir, updateHistoryFileName)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("resolve update history log path: %w", err)
|
||||
}
|
||||
|
||||
return normalizedDir, logPath, nil
|
||||
}
|
||||
|
||||
// NewUpdateHistory creates a new update history manager
|
||||
func NewUpdateHistory(dataDir string) (*UpdateHistory, error) {
|
||||
if dataDir == "" {
|
||||
dataDir = "/var/lib/pulse"
|
||||
resolvedDataDir, logPath, err := resolveUpdateHistoryLogPath(dataDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("create update history data directory %q: %w", dataDir, err)
|
||||
if err := os.MkdirAll(resolvedDataDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("create update history data directory %q: %w", resolvedDataDir, err)
|
||||
}
|
||||
|
||||
logPath := filepath.Join(dataDir, "update-history.jsonl")
|
||||
|
||||
h := &UpdateHistory{
|
||||
logPath: logPath,
|
||||
cache: make([]UpdateHistoryEntry, 0, 100),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,41 @@ func TestNewUpdateHistory(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("canonicalizes explicit data dir", func(t *testing.T) {
|
||||
rawDataDir := filepath.Join(t.TempDir(), "subdir", "..", "nested", ".")
|
||||
|
||||
resolvedDir, logPath, err := resolveUpdateHistoryLogPath(" " + rawDataDir + " ")
|
||||
if err != nil {
|
||||
t.Fatalf("resolveUpdateHistoryLogPath() error = %v", err)
|
||||
}
|
||||
|
||||
expectedDir := filepath.Clean(rawDataDir)
|
||||
if resolvedDir != expectedDir {
|
||||
t.Fatalf("resolved dir = %q, want %q", resolvedDir, expectedDir)
|
||||
}
|
||||
|
||||
expectedLogPath := filepath.Join(expectedDir, updateHistoryFileName)
|
||||
if logPath != expectedLogPath {
|
||||
t.Fatalf("log path = %q, want %q", logPath, expectedLogPath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("whitespace data dir uses default path", func(t *testing.T) {
|
||||
resolvedDir, logPath, err := resolveUpdateHistoryLogPath(" \t ")
|
||||
if err != nil {
|
||||
t.Fatalf("resolveUpdateHistoryLogPath() error = %v", err)
|
||||
}
|
||||
|
||||
if resolvedDir != defaultUpdateHistoryDataDir {
|
||||
t.Fatalf("resolved dir = %q, want %q", resolvedDir, defaultUpdateHistoryDataDir)
|
||||
}
|
||||
|
||||
expectedLogPath := filepath.Join(defaultUpdateHistoryDataDir, updateHistoryFileName)
|
||||
if logPath != expectedLogPath {
|
||||
t.Fatalf("log path = %q, want %q", logPath, expectedLogPath)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("initializes with empty cache", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
h, err := NewUpdateHistory(tmpDir)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue