Unverified Commit 912d129f authored by Lemon-miaow's avatar Lemon-miaow
Browse files

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

parent 34a4dffc
Loading
Loading
Loading
Loading
+2 −1
Changes for cmd/felis/migrate.go: 2 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -18,7 +18,8 @@ import (
// database that already holds a schema and has migrations pending is bundled
// 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
// 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 {
	fs := flag.NewFlagSet("migrate", flag.ContinueOnError)
	fs.SetOutput(stderr)
+6 −4
Changes for cmd/felis/update.go: 6 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -128,8 +128,9 @@ var updateTargets = []updateTarget{
		selector:  "postgres",
		help:      "select 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",
		command:   "sudo dnf upgrade 'postgresql*' || sudo apt-get install --only-upgrade 'postgresql*'; sudo systemctl restart postgresql",
		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:   installerRerun,
		installer: true,
	},
	{
		selector:  "mc",
@@ -149,8 +150,9 @@ var updateTargets = []updateTarget{
// reinstall/repair case.
//
// 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
// PostgreSQL answer `--version`, Velocity's version is read out of the installed jar's
// it needs no root, apart from PostgreSQL's version, which the felis-postgres container
// 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
// build stamp — the same value `felis version` prints, which is what the user asked
// to be the source of truth.
+4 −4
Changes for cmd/felis/update_test.go: 4 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -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
// manager's, so its guidance carries no installer trailer.
// k3s and cloudflared move only when the re-run is told to; the release pins the JRE
// build and the PostgreSQL image, so their guidance is the plain re-run.
func TestApplyGuidanceForHostDependencies(t *testing.T) {
	notify := func(c string) updater.Result {
		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)
	}
	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") {
		t.Errorf("--postgres guidance is the package manager, without the installer trailer:\n%s", pg)
	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 plain installer re-run that moves the image pin:\n%s", pg)
	}
}

+29 −7
Changes for internal/updater/gatherer_host.go: 29 added lines, 7 removed lines.
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ import (
	"path/filepath"
	"strings"

	"felis.lolicon.best/internal/platform"
	"felis.lolicon.best/internal/updates"
)

@@ -86,18 +87,39 @@ func (g hostGatherer) Current(ctx context.Context, spec Spec) (updates.Version,
	case "jre":
		return jreReleaseVersion(g.jreRelease)
	case "postgresql":
		// The server binary is on PATH on the dnf family; Debian and Ubuntu keep it
		// 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")
		return g.postgresVersion(ctx)
	default:
		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
// 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.
+39 −12
Changes for internal/updater/gatherer_host_test.go: 39 added lines, 12 removed lines.
Original line number Diff line number Diff line
@@ -3,8 +3,11 @@ package updater
import (
	"archive/zip"
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"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.
type argvCmd struct {
	want string
	out  []byte
	err  error
	got  *[]string
}

func (a argvCmd) output(_ context.Context, name string, args ...string) ([]byte, error) {
	line := strings.Join(append([]string{name}, args...), " ")
	*a.got = append(*a.got, line)
	if line != a.want {
		return nil, fmt.Errorf("unexpected command %q", line)
	}
	return a.out, a.err
}

// 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) {
	for name, out := range map[string]map[string][]byte{
		"server on PATH": {"postgres": []byte("postgres (PostgreSQL) 13.23\n"), "psql": []byte("psql (PostgreSQL) 12.1\n")},
		"client only":    {"psql": []byte("psql (PostgreSQL) 13.23 (Ubuntu 13.23-1.pgdg24.04+1)\n")},
	} {
		g := hostGatherer{sys: sysGatherer{run: fakeCmd{out: out}}}
	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("%s: %v", name, err)
		t.Fatalf("Current: %v (ran %q)", err, got)
	}
		if v.String() != "13.23" {
			t.Errorf("%s: version = %s, want 13.23", name, v)
	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)
	}
	g := hostGatherer{sys: sysGatherer{run: fakeCmd{out: map[string][]byte{}}}}
	if _, err := g.Current(context.Background(), Spec{Name: "postgresql"}); err == nil {
		t.Error("no postgres and no psql must be an error")

	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)
	}
}
Loading