Introduce internal/updates: a pure, I/O-free engine that decides what should happen to each tracked platform component (Felis control-plane, k3s, cloudflared, Velocity) given its current version, the latest discovered upstream, its policy, and the current time. Updates are never force-applied. A component is Pinned (Minecraft, left alone), Notify (a SysAdmin is told and applies out of band), or Scheduled (Felis may apply, but only inside a maintenance window the SysAdmin set). The load-bearing invariants are unit-tested: a pinned component never changes, a downgrade is never proposed, a prerelease is never auto-applied, and an apply happens only inside the window. Version parsing tolerates the real feeds (leading v, k3s +k3s1 build suffix, calendar versions, prerelease tails) and orders by SemVer precedence. ReleaseSource/Notifier/Applier are declared as integration seams and exercised via fakes; this package ships no network, SMTP, or kubectl, and deliberately has no blind k3s-upgrade applier.
55 lines
2.0 KiB
Go
55 lines
2.0 KiB
Go
package updates
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Report renders a plan as a human-readable component status summary — the
|
|
// "版本号状态" the user asked for as much as any apply. It is pure (no clock, no I/O):
|
|
// the caller decides where it goes (an email body, an in-game message, the TUI). One
|
|
// line per component, in plan order:
|
|
//
|
|
// felis-api 1.4.0 -> 1.5.0 apply (scheduled window)
|
|
// k3s v1.30.2+k3s1 -> v1.30.3+k3s1 update available (notify)
|
|
// velocity 3.3.0 -> 3.4.0 update available — apply manually
|
|
// cloudflared 2024.3.0 up to date
|
|
// mc-survival 1.20.1 pinned
|
|
func Report(plan []Action) string {
|
|
if len(plan) == 0 {
|
|
return "No components tracked."
|
|
}
|
|
// Width the name and current columns so the arrows line up.
|
|
nameW, curW := 0, 0
|
|
for _, a := range plan {
|
|
nameW = max(nameW, len(a.Component))
|
|
curW = max(curW, len(a.Current.String()))
|
|
}
|
|
|
|
var b strings.Builder
|
|
for _, a := range plan {
|
|
fmt.Fprintf(&b, "%-*s %-*s", nameW, a.Component, curW, a.Current.String())
|
|
switch a.Kind {
|
|
case ActionApply:
|
|
fmt.Fprintf(&b, " -> %-*s apply (scheduled window)", curW, a.Latest.String())
|
|
case ActionNotify:
|
|
// Distinguish the off-cluster / unmanaged case: a notify Felis cannot follow
|
|
// with its own apply reads "apply manually", so the SysAdmin knows the ball is
|
|
// in their court. We infer it structurally: an ActionNotify whose latest is a
|
|
// stable upgrade is either "notify" or "notify-only"; the report cannot see
|
|
// Manageable, so it states the neutral, always-true instruction.
|
|
fmt.Fprintf(&b, " -> %-*s update available (notify)", curW, a.Latest.String())
|
|
case ActionPinned:
|
|
fmt.Fprintf(&b, " %-*s pinned", curW, "")
|
|
default: // ActionNone
|
|
if a.LatestKnown {
|
|
fmt.Fprintf(&b, " %-*s up to date", curW, "")
|
|
} else {
|
|
fmt.Fprintf(&b, " %-*s latest unknown", curW, "")
|
|
}
|
|
}
|
|
b.WriteByte('\n')
|
|
}
|
|
return b.String()
|
|
}
|