feat(updater): 从 felis-postgres 容器读取 PostgreSQL 版本并改为随发布更新镜像

This commit is contained in:
Lemon-miaow committed 2026-09-26 12:17:00 +08:00
1 parent 34a4dffc50
commit 912d129f14
7 files changed
+94 -40

No files matched your search

+2 -1
View File
@@ -18,7 +18,8 @@ import (
// database that already holds a schema and has migrations pending is bundled // database that already holds a schema and has migrations pending is bundled
// first (internal/dbbackup, label pre-migrate). A failed snapshot stops the // first (internal/dbbackup, label pre-migrate). A failed snapshot stops the
// upgrade; -no-backup is the explicit way past it, e.g. for an external // upgrade; -no-backup is the explicit way past it, e.g. for an external
// database whose server is newer than the host's pg_dump. // database (no [database] deployment, so the host's own pg_dump runs) whose
// server is newer than that pg_dump.
func cmdMigrate(args []string, stdout, stderr io.Writer) int { func cmdMigrate(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("migrate", flag.ContinueOnError) fs := flag.NewFlagSet("migrate", flag.ContinueOnError)
fs.SetOutput(stderr) fs.SetOutput(stderr)
+6 -4
View File
@@ -128,8 +128,9 @@ var updateTargets = []updateTarget{
selector: "postgres", selector: "postgres",
help: "select PostgreSQL", help: "select PostgreSQL",
component: "postgresql", component: "postgresql",
note: "PostgreSQL comes from the distribution's packages, so a minor release is a package update followed by a restart (a few seconds without the API). A new major needs pg_upgrade first: docs/operations.md §4", note: "PostgreSQL runs as the felis-postgres Deployment from the image the Felis release pins by digest; a newer minor reaches the host with a release that moves the pin, and the installer re-run restarts the database on it (a few seconds without the API). A new major is a dump and restore: docs/operations.md §4",
command: "sudo dnf upgrade 'postgresql*' || sudo apt-get install --only-upgrade 'postgresql*'; sudo systemctl restart postgresql", command: installerRerun,
installer: true,
}, },
{ {
selector: "mc", selector: "mc",
@@ -149,8 +150,9 @@ var updateTargets = []updateTarget{
// reinstall/repair case. // reinstall/repair case.
// //
// It never applies anything and never mutates the node, so unlike setup/breakGlass // It never applies anything and never mutates the node, so unlike setup/breakGlass
// it needs no root. The versions it reads come from this host: k3s, cloudflared and // it needs no root, apart from PostgreSQL's version, which the felis-postgres container
// PostgreSQL answer `--version`, Velocity's version is read out of the installed jar's // answers through the cluster's admin kubeconfig. The versions it reads come from this
// host: k3s, cloudflared and PostgreSQL answer `--version`, Velocity's version is read out of the installed jar's
// manifest, the JRE's out of its release file, and felis-api's is this binary's own // manifest, the JRE's out of its release file, and felis-api's is this binary's own
// build stamp — the same value `felis version` prints, which is what the user asked // build stamp — the same value `felis version` prints, which is what the user asked
// to be the source of truth. // to be the source of truth.
+4 -4
View File
@@ -215,8 +215,8 @@ func TestUpdateSelectorsAreFlags(t *testing.T) {
} }
} }
// k3s and cloudflared move only when the re-run is told to; PostgreSQL is the package // k3s and cloudflared move only when the re-run is told to; the release pins the JRE
// manager's, so its guidance carries no installer trailer. // build and the PostgreSQL image, so their guidance is the plain re-run.
func TestApplyGuidanceForHostDependencies(t *testing.T) { func TestApplyGuidanceForHostDependencies(t *testing.T) {
notify := func(c string) updater.Result { notify := func(c string) updater.Result {
return planResult([]updates.Action{{Component: c, Kind: updates.ActionNotify, LatestKnown: true}}) return planResult([]updates.Action{{Component: c, Kind: updates.ActionNotify, LatestKnown: true}})
@@ -232,8 +232,8 @@ func TestApplyGuidanceForHostDependencies(t *testing.T) {
t.Errorf("--jre guidance is the plain installer re-run:\n%s", jre) t.Errorf("--jre guidance is the plain installer re-run:\n%s", jre)
} }
pg := renderApplyGuidance(notify("postgresql"), map[string]bool{"postgres": true}, false) pg := renderApplyGuidance(notify("postgresql"), map[string]bool{"postgres": true}, false)
if !strings.Contains(pg, "apt-get install --only-upgrade") || strings.Contains(pg, "Re-running the installer") { if !strings.Contains(pg, "| sudo bash") || strings.Contains(pg, "FELIS_UPGRADE_DEPS") || strings.Contains(pg, "apt-get") || strings.Contains(pg, "systemctl") {
t.Errorf("--postgres guidance is the package manager, without the installer trailer:\n%s", pg) t.Errorf("--postgres guidance is the plain installer re-run that moves the image pin:\n%s", pg)
} }
} }
+29 -7
View File
@@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"felis.lolicon.best/internal/platform"
"felis.lolicon.best/internal/updates" "felis.lolicon.best/internal/updates"
) )
@@ -86,18 +87,39 @@ func (g hostGatherer) Current(ctx context.Context, spec Spec) (updates.Version,
case "jre": case "jre":
return jreReleaseVersion(g.jreRelease) return jreReleaseVersion(g.jreRelease)
case "postgresql": case "postgresql":
// The server binary is on PATH on the dnf family; Debian and Ubuntu keep it return g.postgresVersion(ctx)
// under /usr/lib/postgresql/<major>/bin and put only the client on PATH, which
// the distribution ships at the same version.
if v, err := g.sys.cliVersion(ctx, "postgres"); err == nil {
return v, nil
}
return g.sys.cliVersion(ctx, "psql")
default: default:
return g.sys.Current(ctx, spec) return g.sys.Current(ctx, spec)
} }
} }
// postgresVersionArgv asks the server binary in the felis-postgres Deployment
// (internal/platform/postgres.go) for its version. The database runs from the image
// the release pins, so the answer comes from the container: a distribution package
// the move into k3s left installed on the host answers with a version nothing runs.
var postgresVersionArgv = []string{
"kubectl", "exec", "-n", platform.DefaultControlNamespace, "deploy/" + platform.PostgresName,
"-c", platform.PostgresContainer, "--", "postgres", "--version",
}
func (g hostGatherer) postgresVersion(ctx context.Context) (updates.Version, error) {
if g.sys.run == nil {
return updates.Version{}, fmt.Errorf("updater: command runner not wired for %s", platform.PostgresName)
}
out, err := g.sys.run.output(ctx, "k3s", postgresVersionArgv...)
if err != nil {
if msg := truncate(string(out), 200); msg != "" {
err = fmt.Errorf("%w: %s", err, msg)
}
return updates.Version{}, fmt.Errorf("updater: ask %s for its version: %w", platform.PostgresName, err)
}
v, err := versionFromCLI(string(out))
if err != nil {
return updates.Version{}, fmt.Errorf("updater: %s: %w", platform.PostgresName, err)
}
return v, nil
}
// jreReleaseVersion reads the runtime's version from its release file. Temurin writes // jreReleaseVersion reads the runtime's version from its release file. Temurin writes
// SEMANTIC_VERSION="25.0.4.1+1"; JAVA_VERSION="25.0.4.1" is the fallback every JDK // SEMANTIC_VERSION="25.0.4.1+1"; JAVA_VERSION="25.0.4.1" is the fallback every JDK
// build writes. JAVA_RUNTIME_VERSION is avoided: its "-LTS" tail reads as a prerelease. // build writes. JAVA_RUNTIME_VERSION is avoided: its "-LTS" tail reads as a prerelease.
+44 -17
View File
@@ -3,8 +3,11 @@ package updater
import ( import (
"archive/zip" "archive/zip"
"context" "context"
"errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"felis.lolicon.best/internal/updates" "felis.lolicon.best/internal/updates"
@@ -137,23 +140,47 @@ func TestJREReleaseVersion(t *testing.T) {
} }
} }
// PostgreSQL answers from the server binary where it is on PATH, else from the client. // argvCmd answers one exact command line and records what it was asked.
func TestHostGathererPostgres(t *testing.T) { type argvCmd struct {
for name, out := range map[string]map[string][]byte{ want string
"server on PATH": {"postgres": []byte("postgres (PostgreSQL) 13.23\n"), "psql": []byte("psql (PostgreSQL) 12.1\n")}, out []byte
"client only": {"psql": []byte("psql (PostgreSQL) 13.23 (Ubuntu 13.23-1.pgdg24.04+1)\n")}, err error
} { got *[]string
g := hostGatherer{sys: sysGatherer{run: fakeCmd{out: out}}} }
v, err := g.Current(context.Background(), Spec{Name: "postgresql"})
if err != nil { func (a argvCmd) output(_ context.Context, name string, args ...string) ([]byte, error) {
t.Fatalf("%s: %v", name, err) line := strings.Join(append([]string{name}, args...), " ")
} *a.got = append(*a.got, line)
if v.String() != "13.23" { if line != a.want {
t.Errorf("%s: version = %s, want 13.23", name, v) return nil, fmt.Errorf("unexpected command %q", line)
}
} }
g := hostGatherer{sys: sysGatherer{run: fakeCmd{out: map[string][]byte{}}}} return a.out, a.err
if _, err := g.Current(context.Background(), Spec{Name: "postgresql"}); err == nil { }
t.Error("no postgres and no psql must be an error")
// PostgreSQL answers from the server binary in the felis-postgres container, never
// from a host package the move into k3s left installed.
func TestHostGathererPostgres(t *testing.T) {
const exec = "k3s kubectl exec -n felis deploy/felis-postgres -c postgres -- postgres --version"
var got []string
g := hostGatherer{sys: sysGatherer{run: argvCmd{want: exec, out: []byte("postgres (PostgreSQL) 18.6 (Debian 18.6-1.pgdg13+1)\n"), got: &got}}}
v, err := g.Current(context.Background(), Spec{Name: "postgresql"})
if err != nil {
t.Fatalf("Current: %v (ran %q)", err, got)
}
if v.String() != "18.6" {
t.Errorf("version = %s, want 18.6", v)
}
if len(got) != 1 {
t.Errorf("ran %q, want exactly the exec into the database pod", got)
}
got = nil
down := hostGatherer{sys: sysGatherer{run: argvCmd{want: exec, out: []byte("error: no running pod\n"), err: errors.New("exit status 1"), got: &got}}}
_, err = down.Current(context.Background(), Spec{Name: "postgresql"})
if err == nil || !strings.Contains(err.Error(), "no running pod") {
t.Errorf("a database pod that cannot answer = %v, want an error carrying kubectl's reason", err)
}
if len(got) != 1 {
t.Errorf("ran %q after the pod failed, want no fallback to host binaries", got)
} }
} }
+5 -5
View File
@@ -17,12 +17,12 @@ import (
// major, e.g. {"major":"13","latestMinor":"23","supported":false,"eolDate":"2025-11-13"}. // major, e.g. {"major":"13","latestMinor":"23","supported":false,"eolDate":"2025-11-13"}.
const postgresFeedURL = "https://www.postgresql.org/versions.json" const postgresFeedURL = "https://www.postgresql.org/versions.json"
// postgresFeed discovers the newest minor release of the host's PostgreSQL major. // postgresFeed discovers the newest minor release of the running PostgreSQL major.
// //
// Felis installs PostgreSQL from the distribution, so the major is whatever the // Felis runs PostgreSQL from an image the release pins, and moving to a new major is a
// distribution ships and moving it is a pg_upgrade the operator plans. The report // dump and restore the operator plans. The report therefore compares within the major
// therefore compares within the major (a minor release is a package update), and a // (a minor release is a new pin), and a major that is past its end of life is surfaced
// major that is past its end of life is surfaced separately as a note. // separately as a note.
type postgresFeed struct { type postgresFeed struct {
url string url string
userAgent string userAgent string
+4 -2
View File
@@ -44,8 +44,10 @@ type Spec struct {
// come from PaperMC (Fill v3), not GitHub. // come from PaperMC (Fill v3), not GitHub.
// - jre — the Temurin runtime Velocity runs on. The installer pins its patch // - jre — the Temurin runtime Velocity runs on. The installer pins its patch
// build, so a newer build reaches a host through a Felis release: Notify only. // build, so a newer build reaches a host through a Felis release: Notify only.
// - postgresql — the control-plane database, from the distribution's packages. // - postgresql — the control-plane database, the felis-postgres Deployment running
// Notify only; a minor release is a package update, a major one a pg_upgrade. // the image the installer pins by digest. Notify only: a newer minor reaches a
// host through a Felis release that moves the pin, a major one through a dump and
// restore.
// //
// Minecraft is deliberately ABSENT: every MC server is Pinned and is appended to the // 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. // plan at runtime from the live fleet (integration), never force-tracked here.