controller v0.269.0: whole restore from the second drive; crash loops stopped; exact image digests; steps judged by their own .felhom.yml (decisions 26-28, R-661 R-666 R-667 R-668 R-664 R-665 R-662, 09 6.4 part 6)
gates / gates (push) Successful in 27s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-24 12:18:39 +02:00
parent 7c3b3a9694
commit 3c6b49b31c
141 changed files with 3401 additions and 237 deletions
+24 -3
View File
@@ -170,9 +170,12 @@ type Stack struct {
HoldReason string `json:"hold_reason,omitempty"`
// HoldNoWholeCopy (v0.268.0, R-659): the hold names NO copy — none on this box brings the app back
// whole — so the page offers no restore button beside the sentence (the restore would refuse).
HoldNoWholeCopy bool `json:"hold_no_whole_copy,omitempty"`
HealthProbe *HealthProbeResult `json:"health_probe,omitempty"` // controller-side probe result
LastUpdated time.Time `json:"last_updated"`
HoldNoWholeCopy bool `json:"hold_no_whole_copy,omitempty"`
// HoldKind (v0.269.0): which hold — "update_failed", "unhealthy_stop" (decision 28: the page offers
// Start, not a restore), "restore"; "" when none.
HoldKind string `json:"hold_kind,omitempty"`
HealthProbe *HealthProbeResult `json:"health_probe,omitempty"` // controller-side probe result
LastUpdated time.Time `json:"last_updated"`
// RestartingSince (C9-F2) is when this stack was FIRST observed in StateRestarting during the
// current restarting run; zero whenever the stack is in any other state. It is what turns a brief
// restart (normal: deploy, update, quiesce restart) into a distinguishable crash loop — see
@@ -205,10 +208,22 @@ type Stack struct {
// pin and the catalog's head; one press climbs one. 0 = unknown or none. The page shows it while
// the badge says „Frissítés elérhető".
LadderStepsLeft int `json:"ladder_steps_left,omitempty"`
// CatalogDigests / CatalogTestedAt (v0.269.0, `09` §6.4 part 6): the TESTED digest per service of the
// catalog's current refs and when that test ran — the badge's input for a floating tag (never a
// registry query). Empty when the ladder has no entry for them.
CatalogDigests map[string]string `json:"catalog_digests,omitempty"`
CatalogTestedAt time.Time `json:"catalog_tested_at,omitempty"`
}
// Manager handles all docker compose stack operations.
type Manager struct {
// lastVerifyMeta (v0.269.0) is the .felhom.yml the last update verify judged by — read by a test.
lastVerifyMeta string
// updateHealthMetaFn (test seam, v0.269.0): the verify with the chosen .felhom.yml; nil → the real wait.
updateHealthMetaFn func(ctx context.Context, name string, timeout time.Duration, meta *Metadata) (bool, string)
// unhealthy (v0.269.0, decision 28): RestartCount / OOM-kill samples per app for the crash-loop stop.
unhealthy unhealthyWatch
// selfUpdating (v0.261.0) reports whether the CONTROLLER is swapping itself. Set by
// SetSelfUpdatingCheck; nil means no gate. See update.go.
selfUpdating func() bool
@@ -611,6 +626,8 @@ func (m *Manager) ScanStacks() error {
// catalog template by definition, and warning once per app per scan would be noise.
var catImages map[string]string
stepsLeft := 0
var catDigests map[string]string
var catTestedAt time.Time
if deployed && !m.cfg.IsProtectedStack(name) {
catPath := m.CatalogTemplatePath(name, "docker-compose.yml")
if appCfg != nil {
@@ -618,6 +635,7 @@ func (m *Manager) ScanStacks() error {
}
if imgs, cerr := ParseComposeImages(catPath); cerr == nil {
catImages = imgs
catDigests, catTestedAt = catalogTestedDigests(filepath.Dir(catPath), imgs)
} else if m.isDebug() {
m.logger.Printf("[DEBUG] [stacks] ScanStacks: no readable catalog template for %s (%v) — the update badge will render nothing", name, cerr)
}
@@ -635,6 +653,7 @@ func (m *Manager) ScanStacks() error {
existing.TemplateImages = tplImages
existing.CatalogImages = catImages
existing.LadderStepsLeft = stepsLeft
existing.CatalogDigests, existing.CatalogTestedAt = catDigests, catTestedAt
}
} else {
m.stacks[name] = &Stack{
@@ -648,6 +667,8 @@ func (m *Manager) ScanStacks() error {
TemplateImages: tplImages,
CatalogImages: catImages,
LadderStepsLeft: stepsLeft,
CatalogDigests: catDigests,
CatalogTestedAt: catTestedAt,
}
}
}