feat: add controller self-update mechanism (v0.16.0)
New selfupdate package: version parsing, audit state file, updater with
Gitea registry V2 check, docker pull + compose rewrite + compose up flow.
- API: /api/selfupdate/{status,check,update} with session+bearer auth
- UI: Settings "Verzió és frissítés" card with check/install buttons + JS polling
- Scheduler: periodic check (6h default) + optional daily auto-update
- Notifications: success/failure on post-update startup verification
- Alert: info banner when update available
- docker-compose.yml: add directory bind mount for compose file access
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package selfupdate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const stateFileName = "update-state.json"
|
||||
|
||||
// UpdateState tracks the last update attempt. Persisted to disk as audit log.
|
||||
type UpdateState struct {
|
||||
Status string `json:"status"` // "pending", "success", "failed"
|
||||
PreviousVersion string `json:"previous_version"`
|
||||
PreviousImage string `json:"previous_image"`
|
||||
TargetVersion string `json:"target_version"`
|
||||
TargetImage string `json:"target_image"`
|
||||
InitiatedAt string `json:"initiated_at"` // RFC3339
|
||||
InitiatedBy string `json:"initiated_by"` // "manual" or "auto"
|
||||
CompletedAt string `json:"completed_at,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// LoadState reads the update state file. Returns nil, nil if file doesn't exist.
|
||||
func LoadState(dataDir string) (*UpdateState, error) {
|
||||
path := filepath.Join(dataDir, stateFileName)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("reading state file: %w", err)
|
||||
}
|
||||
|
||||
var state UpdateState
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
return nil, fmt.Errorf("parsing state file: %w", err)
|
||||
}
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
// SaveState writes the state file atomically (write to .tmp, then rename).
|
||||
func SaveState(dataDir string, state *UpdateState) error {
|
||||
path := filepath.Join(dataDir, stateFileName)
|
||||
tmpPath := path + ".tmp"
|
||||
|
||||
data, err := json.MarshalIndent(state, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling state: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(tmpPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("writing temp state file: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpPath, path); err != nil {
|
||||
return fmt.Errorf("renaming state file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearState removes the state file. Used for cleanup.
|
||||
func ClearState(dataDir string, logger *log.Logger) {
|
||||
path := filepath.Join(dataDir, stateFileName)
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
logger.Printf("[WARN] Failed to clear update state file: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user