88b0a2e761
gates / gates (push) Successful in 30s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
201 lines
6.4 KiB
Go
201 lines
6.4 KiB
Go
package osupdates
|
|
|
|
import (
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
|
)
|
|
|
|
type fix struct {
|
|
s *Service
|
|
now time.Time
|
|
events []string
|
|
bumps []string
|
|
}
|
|
|
|
func newFix(t *testing.T) *fix {
|
|
t.Helper()
|
|
st, err := store.New(filepath.Join(t.TempDir(), "hub.db"), log.New(os.Stderr, "", 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { st.Close() })
|
|
for _, h := range []struct{ host, cust string }{{"hp", "c-hp"}, {"n100", "c-n100"}, {"cust1", "c-1"}} {
|
|
if err := st.UpsertHost(&store.Host{HostID: h.host, CustomerID: h.cust, APIKey: "k-" + h.host}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
f := &fix{now: time.Date(2026, 10, 4, 12, 0, 0, 0, time.UTC)}
|
|
f.s = &Service{Store: st, ApproveAfter: 24 * time.Hour, NightsRequired: 1, Now: func() time.Time { return f.now },
|
|
Emit: func(_, typ, _, _, _, _ string) { f.events = append(f.events, typ) },
|
|
Bump: func(h string) { f.bumps = append(f.bumps, h) }}
|
|
_ = st.SetOSRing("hp", 0)
|
|
_ = st.SetOSRing("n100", 0)
|
|
return f
|
|
}
|
|
|
|
func pk(name, ver string) Package { return Package{Name: name, Version: ver, Origin: "Debian"} }
|
|
|
|
func (f *fix) report(t *testing.T, host, trigger string, healthy bool, pkgs ...Package) {
|
|
t.Helper()
|
|
f.reportL(t, host, LayerGuest, trigger, healthy, pkgs...)
|
|
}
|
|
|
|
func (f *fix) reportL(t *testing.T, host, layer, trigger string, healthy bool, pkgs ...Package) {
|
|
t.Helper()
|
|
outcome := "applied"
|
|
if !healthy {
|
|
outcome = "health_failed"
|
|
}
|
|
if err := f.s.Ingest(host, Report{RunID: host + trigger + f.now.String(), Layer: layer, Trigger: trigger, Mode: "apply", Outcome: outcome,
|
|
Healthy: healthy, Installed: pkgs, Upgraded: pkgs[:1]}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// The ruled wait: approved only after every ring-0 box ran the set healthy for ApproveAfter AND through a night run.
|
|
// Red-proof: drop the age check, the healthy check or the nights check in Evaluate and a sub-step fails.
|
|
func TestApproval_WaitHealthyAndOneNight(t *testing.T) {
|
|
f := newFix(t)
|
|
set := []Package{pk("libc6", "2.41-12+deb13u4"), pk("openssl", "3.5.7-1~deb13u3")}
|
|
f.report(t, "hp", "debug", true, set...)
|
|
f.report(t, "n100", "debug", true, set...)
|
|
sts, _ := f.s.Evaluate()
|
|
st := sts[0]
|
|
if !strings.HasPrefix(st.Waiting, "healthy for") {
|
|
t.Fatalf("fresh set approved or wrong reason: %+v", st)
|
|
}
|
|
f.now = f.now.Add(25 * time.Hour)
|
|
sts, _ = f.s.Evaluate()
|
|
st = sts[0]
|
|
if !strings.Contains(st.Waiting, "night run") {
|
|
t.Fatalf("approved without a night run: %+v", st)
|
|
}
|
|
f.report(t, "hp", "night", true, set...)
|
|
f.report(t, "n100", "night", true, set...)
|
|
if _, err := f.s.Evaluate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rel, _ := f.s.Store.LatestOSRelease(LayerGuest)
|
|
if rel == nil || rel.ApprovedBy != "auto" {
|
|
t.Fatalf("not approved: %+v", rel)
|
|
}
|
|
if len(f.bumps) != 1 || f.bumps[0] != "cust1" {
|
|
t.Fatalf("only the ring-1 box must be nudged, got %v", f.bumps)
|
|
}
|
|
b := f.s.DesiredBlock("cust1")
|
|
if b.Ring != 1 || !b.Enabled || b.Release == nil || len(b.Release.Packages) != 2 || b.Release.Snapshot != rel.ApprovedAt.UTC().Format("20060102T150405Z") {
|
|
t.Fatalf("ring-1 block = %+v", b)
|
|
}
|
|
if b.HostRelease != nil {
|
|
t.Fatalf("a guest approval must not create a host release (separate sets): %+v", b.HostRelease)
|
|
}
|
|
if hb := f.s.DesiredBlock("hp"); hb.Ring != 0 || hb.Release != nil {
|
|
t.Fatalf("a ring-0 box must not get a release (it installs everything pending): %+v", hb)
|
|
}
|
|
}
|
|
|
|
func TestApproval_UnhealthyRunBlocks(t *testing.T) {
|
|
f := newFix(t)
|
|
set := []Package{pk("libc6", "2.41-12+deb13u4")}
|
|
f.report(t, "hp", "debug", true, set...)
|
|
f.report(t, "n100", "debug", true, set...)
|
|
f.s.Evaluate()
|
|
f.now = f.now.Add(25 * time.Hour)
|
|
f.report(t, "hp", "night", false, set...)
|
|
f.report(t, "n100", "night", true, set...)
|
|
sts, _ := f.s.Evaluate()
|
|
st := sts[0]
|
|
if rel, _ := f.s.Store.LatestOSRelease(LayerGuest); rel != nil {
|
|
t.Fatalf("approved although a ring-0 run was not healthy: %+v", st)
|
|
}
|
|
if !strings.Contains(st.Waiting, "healthy=false") {
|
|
t.Fatalf("reason = %q", st.Waiting)
|
|
}
|
|
found := false
|
|
for _, e := range f.events {
|
|
found = found || e == EventHealthFailed
|
|
}
|
|
if !found {
|
|
t.Fatalf("no %s event: %v", EventHealthFailed, f.events)
|
|
}
|
|
}
|
|
|
|
// Ring 0 disagreeing on a package: that package is left out of the candidate (C9 — approve what ALL ring 0 runs).
|
|
func TestCandidate_DisagreementLeftOut(t *testing.T) {
|
|
f := newFix(t)
|
|
f.report(t, "hp", "night", true, pk("libc6", "2.41-12+deb13u4"), pk("curl", "8.14.1-2+deb13u5"))
|
|
f.report(t, "n100", "night", true, pk("libc6", "2.41-12+deb13u4"), pk("curl", "8.14.1-2+deb13u4"))
|
|
cand, err := f.s.candidate(LayerGuest, []string{"hp", "n100"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := cand["curl"]; ok || cand["libc6"].Version != "2.41-12+deb13u4" {
|
|
t.Fatalf("candidate = %+v", cand)
|
|
}
|
|
}
|
|
|
|
// Non-Debian origins never enter a release (the fast lane is Debian / Debian-Security only, C3).
|
|
func TestCandidate_OnlyDebianOrigins(t *testing.T) {
|
|
f := newFix(t)
|
|
d := Package{Name: "docker-ce", Version: "5:29.8.2", Origin: "Docker"}
|
|
f.report(t, "hp", "night", true, pk("libc6", "x1"), d)
|
|
f.report(t, "n100", "night", true, pk("libc6", "x1"), d)
|
|
cand, _ := f.s.candidate(LayerGuest, []string{"hp", "n100"})
|
|
if _, ok := cand["docker-ce"]; ok {
|
|
t.Fatal("a Docker package entered the candidate")
|
|
}
|
|
}
|
|
|
|
func TestApproveNow_IsAnOperatorEvent(t *testing.T) {
|
|
f := newFix(t)
|
|
f.report(t, "hp", "debug", true, pk("libc6", "x1"))
|
|
f.report(t, "n100", "debug", true, pk("libc6", "x1"))
|
|
id, err := f.s.ApproveNow()
|
|
if err != nil || id == "" {
|
|
t.Fatalf("%q %v", id, err)
|
|
}
|
|
rel, _ := f.s.Store.LatestOSRelease(LayerGuest)
|
|
if rel.ApprovedBy != "operator" {
|
|
t.Fatalf("approved_by = %q", rel.ApprovedBy)
|
|
}
|
|
n := 0
|
|
for _, e := range f.events {
|
|
if e == EventApprovedNow {
|
|
n++
|
|
}
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("want one %s, got %v", EventApprovedNow, f.events)
|
|
}
|
|
}
|
|
|
|
func TestSwitchAndRing(t *testing.T) {
|
|
f := newFix(t)
|
|
if err := f.s.SetEnabled("cust1", false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if b := f.s.DesiredBlock("cust1"); b.Enabled {
|
|
t.Fatal("switch OFF not delivered")
|
|
}
|
|
if err := f.s.SetRing("cust1", 2); err == nil {
|
|
t.Fatal("ring 2 accepted")
|
|
}
|
|
if b := f.s.DesiredBlock("nobody-row"); b.Ring != 1 || !b.Enabled {
|
|
t.Fatalf("default must be ring 1, ON: %+v", b)
|
|
}
|
|
}
|
|
|
|
func TestIngest_AppliedIsTheHouseholdsLine(t *testing.T) {
|
|
f := newFix(t)
|
|
f.report(t, "cust1", "night", true, pk("libc6", "x1"))
|
|
if len(f.events) != 1 || f.events[0] != EventApplied {
|
|
t.Fatalf("events = %v", f.events)
|
|
}
|
|
}
|