v0.279.0: after_install (decision 45), known default logins on the page, Part D empty-backup alarm, night chain (R-705), R-706
gates / gates (push) Successful in 27s
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:
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// v0.279.0 (decision 45): the deploy-done hook — the ONE place a fresh install ends — runs after_install, and
|
||||
// the hook is set on every box, not only inside `if notifier != nil` (a box with no hub still installs apps).
|
||||
// COMPANION RED-PROOF: move SetDeployDoneHook back inside `if notifier != nil` → the second assertion fails;
|
||||
// drop the RunAfterInstall call → the first fails.
|
||||
func TestAfterInstall_IsWiredToEveryFreshInstall(t *testing.T) {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "main.go", nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var hook *ast.CallExpr
|
||||
var insideNotifierIf bool
|
||||
var walk func(n ast.Node, underNotifier bool)
|
||||
walk = func(n ast.Node, underNotifier bool) {
|
||||
ast.Inspect(n, func(x ast.Node) bool {
|
||||
if ifs, ok := x.(*ast.IfStmt); ok {
|
||||
if be, ok := ifs.Cond.(*ast.BinaryExpr); ok {
|
||||
if id, ok := be.X.(*ast.Ident); ok && id.Name == "notifier" && be.Op == token.NEQ {
|
||||
walk(ifs.Body, true)
|
||||
if ifs.Else != nil {
|
||||
walk(ifs.Else, underNotifier)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if c, ok := x.(*ast.CallExpr); ok {
|
||||
if s, ok := c.Fun.(*ast.SelectorExpr); ok && s.Sel.Name == "SetDeployDoneHook" && hook == nil {
|
||||
hook, insideNotifierIf = c, underNotifier
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
walk(f, false)
|
||||
if hook == nil {
|
||||
t.Fatal("SetDeployDoneHook is never called")
|
||||
}
|
||||
calls := false
|
||||
ast.Inspect(hook, func(x ast.Node) bool {
|
||||
if s, ok := x.(*ast.SelectorExpr); ok && s.Sel.Name == "RunAfterInstall" {
|
||||
calls = true
|
||||
}
|
||||
return true
|
||||
})
|
||||
if !calls {
|
||||
t.Fatal("the deploy-done hook does not run RunAfterInstall")
|
||||
}
|
||||
if insideNotifierIf {
|
||||
t.Fatal("SetDeployDoneHook is set only when a notifier exists — a box with no hub would never run after_install")
|
||||
}
|
||||
}
|
||||
@@ -1191,6 +1191,18 @@ func main() {
|
||||
// It rides the EXISTING per-run digest (backup_run_failures) rather than a new event type: a
|
||||
// new type is a two-repo change (the hub's allowedEventTypes drops anything unlisted), and the
|
||||
// digest is already operator-only and already means "this run did not fully do its job".
|
||||
// Part D (v0.279.0): a RUNNING app whose newest copy holds no database dump and no volume tar. Rides
|
||||
// the same operator-only digest as R-203 (no new event type — the hub drops unlisted ones), once per
|
||||
// app per tier per day (the backup package dedupes).
|
||||
backupMgr.SetHollowCopyNotify(func(app, tier string) {
|
||||
d := notify.BackupRunFailuresDetails{RunKind: "data-check", Attempted: 1, Failed: 1,
|
||||
Apps: []notify.RunFailureDetail{{App: app, Leg: "hollow-" + tier,
|
||||
Reason: "the app is running but its newest " + tier + " copy holds no database dump and no volume tar"}}}
|
||||
notifier.NotifyBackupRunFailures(fmt.Sprintf(
|
||||
"Backup holds NO DATA: %s is running, but its newest %s copy carries no database dump and no volume tar — "+
|
||||
"the app is not protected on that tier. Check for a hold on a running app (R-704's shape) or a skipped dump.",
|
||||
app, tier), d)
|
||||
})
|
||||
backupMgr.SetOffboxGapNotify(func(gaps map[string][]string) {
|
||||
apps := make([]string, 0, len(gaps))
|
||||
for app := range gaps {
|
||||
@@ -1749,19 +1761,31 @@ func main() {
|
||||
// R-536: „Alkalmazás telepítve" is sent when the async deploy ACTUALLY finishes, and a deploy
|
||||
// that ends badly now says so instead of being silence. The accept-time event is
|
||||
// `app_deploy_started`, emitted by the API router beside its 202.
|
||||
if notifier != nil {
|
||||
stackMgr.SetDeployDoneHook(func(name string, ok bool, detail string) {
|
||||
display := name
|
||||
if s, found := stackMgr.GetStack(name); found && s.Meta.DisplayName != "" {
|
||||
display = s.Meta.DisplayName
|
||||
}
|
||||
if ok {
|
||||
// v0.279.0: the hook is set on EVERY box — after_install (decision 45) must run on a box with no hub too;
|
||||
// only the events need the notifier.
|
||||
stackMgr.SetDeployDoneHook(func(name string, ok bool, detail string) {
|
||||
display := name
|
||||
if s, found := stackMgr.GetStack(name); found && s.Meta.DisplayName != "" {
|
||||
display = s.Meta.DisplayName
|
||||
}
|
||||
if ok {
|
||||
if notifier != nil {
|
||||
notifier.NotifyAppDeployed(name, display)
|
||||
return
|
||||
}
|
||||
// v0.279.0 (decision 45): a FRESH install replaces a known default login with its generated one,
|
||||
// when the template declares after_install. Never on a restore or a kept-data load — those do not
|
||||
// pass through the deploy-done hook.
|
||||
go func() {
|
||||
if ran, err := stackMgr.RunAfterInstall(name, 10*time.Minute); ran && err != nil {
|
||||
log.Printf("[WARN] [stacks] after_install %s: %v", name, err)
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
if notifier != nil {
|
||||
notifier.NotifyAppDeployFailed(name, display, detail)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
// R-681 (v0.270.0): an install a restart cut off is finished through the failure path and reported —
|
||||
// AFTER the deploy-done hook exists, so its event is sent.
|
||||
if names := stackMgr.RecoverInterruptedInstalls(); len(names) > 0 {
|
||||
|
||||
Reference in New Issue
Block a user