Files
Felis/internal/updater/topology.go
T
flyemoji 7db57b9fff feat(updater): add VersionGatherer extraction core and CLI gather seam
Give the Runner a way to read each component's CURRENT version so it can be
compared against the release sources already wired. Three pure extractors turn
raw system text into an updates.Version, each fail-closed:

  - versionFromCLI      — a `<tool> --version` banner   (k3s, cloudflared)
  - versionFromImageRef — a container image tag         (felis-api)
  - versionFromJarName  — a proxy jar filename          (velocity)

sysGatherer routes each Topology component to the right extractor over an
injected seam; every path is exercised with a fake runner, mirroring how the
release sources are proven against httptest.

The load-bearing case is k3s: its Git tag "v1.36.2+k3s1" parses stable, but a
registry cannot store '+', so the same build ships as image tag "v1.36.2-k3s1",
which parses as a prerelease unless repaired. versionFromImageRef normalizes
"-k3sN"/"-rke2rN" back to "+", so an image read and a CLI read agree instead of
the image masquerading as a prerelease and being barred from comparison.

Honest runtime state after this slice — a green suite is not "the updater runs
against real infra": only the CLI seam (execRunner) is wired, so of the four
tracked components just cloudflared is live end to end (gatherable AND
Scheduled/appliable). k3s is CLI-gatherable but Notify-only. felis-api and
velocity are NOT yet runtime-gatherable: their producing seams — a k8s read of
the control-plane Deployment image, and an off-cluster jar inspection — are left
nil, so both surface an explicit "gather seam not wired" error rather than a
wrong version. felis-api self-update is therefore not functional yet.

Remaining integration (tracked in doc.go): the two producing seams, the concrete
Notifier (SMTP + in-game), the Applier (image bump, cloudflared swap), the
`felis update` CLI + CronJob entry point, and the runtime append of the Pinned
Minecraft fleet.
2026-07-05 04:05:56 +09:00

59 lines
3.2 KiB
Go

package updater
import "felis.lolicon.best/internal/updates"
// sourceKind is how a component's latest upstream version is discovered.
type sourceKind int
const (
sourceNone sourceKind = iota // pinned components are never queried
sourceGitHub // GitHub Releases (Coord = "owner/repo")
sourcePaperMC // PaperMC Fill v3 (Coord = project id)
)
// Spec is one platform component's static update policy plus how to find its latest
// upstream version. Current is deliberately NOT here — it is gathered at runtime
// (integration: an image tag, `k3s --version`, a jar manifest) and combined with the
// Spec to form an updates.Component. The topology is the pure, testable expression of
// the user's stated decisions: what Felis keeps current, and how aggressively.
type Spec struct {
Name string
Policy updates.Policy
Manageable bool
Source sourceKind
// Coord is the source-specific coordinate: "owner/repo" for GitHub, the project
// id for PaperMC, empty for pinned components.
Coord string
}
// Topology returns the fixed platform components Felis tracks, each with the update
// policy the user set. The two user red lines shape every entry: "不要强制自动更新"
// (nothing is force-upgraded — the strongest policy is Scheduled, gated on a
// SysAdmin window) and "能不动的就别动" (Minecraft is always pinned).
//
// - felis-api — the control plane Felis ships. Felis MANAGES it (image bump +
// rollout), so Scheduled: applied only inside a SysAdmin-set window, else notify.
// - k3s — the single node the whole platform runs on. Upgrading it is
// high-blast-radius, so Notify only and NOT manageable: Felis reads its latest to
// tell the SysAdmin but never applies it; a human drives the node upgrade.
// - cloudflared — the edge tunnel binary + service Felis manages, so Scheduled.
// - velocity — the proxy, but off-cluster on an admin-operated macvlan host, so
// NOT manageable: even under a schedule it can only ever be Notify. Its releases
// come from PaperMC (Fill v3), not GitHub.
//
// Minecraft is deliberately ABSENT: every MC server is Pinned and is appended to the
// plan at runtime from the live fleet (integration), never force-tracked here.
// Keeping Minecraft out of the static topology is the code-level expression of the
// pin — there is no policy path by which Topology can propose changing it.
func Topology() []Spec {
return []Spec{
// Coord "felis/felis" is a placeholder for the operator's own release repo: the
// GitHub source now consumes it, so the routing/parse path is live, but it will
// not resolve against real GitHub until the operator's actual repo slug is set.
{Name: "felis-api", Policy: updates.PolicyScheduled, Manageable: true, Source: sourceGitHub, Coord: "felis/felis"},
{Name: "k3s", Policy: updates.PolicyNotify, Manageable: false, Source: sourceGitHub, Coord: "k3s-io/k3s"},
{Name: "cloudflared", Policy: updates.PolicyScheduled, Manageable: true, Source: sourceGitHub, Coord: "cloudflare/cloudflared"},
{Name: "velocity", Policy: updates.PolicyNotify, Manageable: false, Source: sourcePaperMC, Coord: "velocity"},
}
}