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

feat(update): felis update 报告 JRE 与 PostgreSQL(含停更提示),bootstrap 支持...

feat(update): felis update 报告 JRE 与 PostgreSQL(含停更提示),bootstrap 支持 FELIS_UPGRADE_DEPS=1 升级 k3s/cloudflared
parent 6f7b8d7b
Loading
Loading
Loading
Loading
+87 −20
Changes for cmd/felis/update.go: 87 added lines, 20 removed lines.
Original line number Diff line number Diff line
@@ -34,6 +34,8 @@ const updateTimeout = 60 * time.Second
type updateTarget struct {
	// selector is the flag name without dashes.
	selector string
	// help is the flag's usage line.
	help string
	// component is the updates planner's name for this piece, or "" when the planner
	// deliberately does not track it (Minecraft, which is pinned).
	component string
@@ -41,9 +43,11 @@ type updateTarget struct {
	note string
	// command is the exact, already-tested way to apply it.
	command string
	// installer marks a command that re-runs the installer, which the trailer explains.
	installer bool
}

// installerRerun is the tested apply path for every planner-backed selector: re-run the
// installerRerun is the tested apply path for every selector Felis installs: re-run the
// installer. It is idempotent, and it is the only path that fetches a newer version --
// `felis setup` skips its host-bootstrap phase on a completed install (all four install
// markers already exist), so there it opens the config console and moves no component,
@@ -58,6 +62,10 @@ type updateTarget struct {
// below points at the README's token'd form for that case.
const installerRerun = "curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/{ref}/deploy/bootstrap.sh | sudo bash"

// installerRerunDeps is the same re-run with FELIS_UPGRADE_DEPS=1, which lets it move an
// installed k3s and cloudflared to the versions the release pins.
const installerRerunDeps = "curl -fsSL https://raw.githubusercontent.com/FelisMC/Felis/{ref}/deploy/bootstrap.sh | sudo FELIS_UPGRADE_DEPS=1 bash"

// updateTargets is the selector table. panel and plugins both resolve to felis-api
// because they are not separately versioned: the panel is compiled into the felis
// binary with //go:embed, and the plugin jars are built from this same repo in the
@@ -65,24 +73,62 @@ const installerRerun = "curl -fsSL https://raw.githubusercontent.com/FelisMC/Fel
var updateTargets = []updateTarget{
	{
		selector:  "panel",
		help:      "select the panel + control plane (felis-api)",
		component: "felis-api",
		note:      "the panel is embedded in the felis binary (//go:embed), so updating it means rebuilding the felis image and rolling felis-api",
		command:   installerRerun,
		installer: true,
	},
	{
		selector:  "velocity",
		help:      "select the Velocity proxy",
		component: "velocity",
		note:      "re-runs install_velocity: the build the release pins in deploy/game-stack.lock (FELIS_VELOCITY_VERSION=<minor> takes that minor's newest build instead), sha256-checked, atomic jar install, then restarts felis-velocity only if the jar or its config changed",
		command:   installerRerun,
		installer: true,
	},
	{
		selector:  "plugins",
		help:      "select the Felis plugin jars (velocity/paper/limbo)",
		component: "felis-api",
		note:      "felis-velocity.jar is a host-file swap, but felis-paper.jar and felis-limbo.jar are baked into the lobby/limbo images and need a rebuild + re-mirror into the in-cluster registry (the installer re-run does both)",
		command:   installerRerun,
		installer: true,
	},
	{
		selector:  "k3s",
		help:      "select k3s",
		component: "k3s",
		note:      "FELIS_UPGRADE_DEPS=1 moves k3s to the version the Felis release pins, which can trail the newest upstream; it moves one minor version at a time and refuses a larger jump. Running game servers keep running while k3s restarts",
		command:   installerRerunDeps,
		installer: true,
	},
	{
		selector:  "cloudflared",
		help:      "select cloudflared",
		component: "cloudflared",
		note:      "FELIS_UPGRADE_DEPS=1 swaps the binary for the sha256-pinned build the Felis release names and restarts cloudflared-felis; the panel's tunnel drops for a few seconds",
		command:   installerRerunDeps,
		installer: true,
	},
	{
		selector:  "jre",
		help:      "select the Temurin JRE Velocity runs on",
		component: "jre",
		note:      "the installer installs the Temurin build the Felis release pins (sha256-checked) and restarts felis-velocity when it changed; a newer upstream build reaches the host with a release that pins it",
		command:   installerRerun,
		installer: true,
	},
	{
		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",
	},
	{
		selector:  "mc",
		help:      "select Minecraft (pinned; reported only)",
		component: "", // never tracked: see the pin note below
		note:      "Minecraft is pinned by policy (\"能不动的就别动\") and Felis never proposes a version change for it. A server's version is a property of that server's image — change it on the server, not through a platform update",
		command:   "",
@@ -92,23 +138,24 @@ var updateTargets = []updateTarget{
// cmdUpdate reports what can be updated and what is already current.
//
// Bare `felis update` prints the status of every tracked component. Selector flags
// (--panel/--velocity/--mc/--plugins/--all) narrow that report to the components
// (--panel/--velocity/--plugins/--k3s/--cloudflared/--jre/--postgres/--mc/--all) narrow that report to the components
// they name AND print how to apply each one. --force additionally prints the apply
// instruction for a selected component that is already up to date, for the
// 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 and cloudflared
// answer `--version`, Velocity's version is read out of the installed jar's
// manifest, 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.
// 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
// 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.
func cmdUpdate(args []string, stdout, stderr io.Writer) int {
	fs := flag.NewFlagSet("update", flag.ContinueOnError)
	fs.SetOutput(stderr)
	panel := fs.Bool("panel", false, "select the panel + control plane (felis-api)")
	velocity := fs.Bool("velocity", false, "select the Velocity proxy")
	mc := fs.Bool("mc", false, "select Minecraft (pinned; reported only)")
	plugins := fs.Bool("plugins", false, "select the Felis plugin jars (velocity/paper/limbo)")
	flags := map[string]*bool{}
	for _, t := range updateTargets {
		flags[t.selector] = fs.Bool(t.selector, false, t.help)
	}
	all := fs.Bool("all", false, "select every component above")
	force := fs.Bool("force", false, "print the apply command for a selected component even when it is already up to date")
	velocityJar := fs.String("velocity-jar", updater.DefaultVelocityJarPath, "path to the installed Velocity jar to read the current version from")
@@ -121,8 +168,8 @@ func cmdUpdate(args []string, stdout, stderr io.Writer) int {
	}

	selected := map[string]bool{}
	for sel, on := range map[string]bool{"panel": *panel, "velocity": *velocity, "mc": *mc, "plugins": *plugins} {
		if on || *all {
	for sel, on := range flags {
		if *on || *all {
			selected[sel] = true
		}
	}
@@ -130,9 +177,10 @@ func cmdUpdate(args []string, stdout, stderr io.Writer) int {
	ctx, cancel := context.WithTimeout(context.Background(), updateTimeout)
	defer cancel()

	src := updater.NewRoutingSource(updater.Topology())
	rn := &updater.Runner{
		Gatherer: updater.NewHostGatherer(resolvedVersion(), *velocityJar),
		Source:   updater.NewRoutingSource(updater.Topology()),
		Source:   src,
		// Notifier and Applier stay nil on purpose: a human typing this command IS the
		// notification, and nothing here applies. The zero Window below means every
		// Scheduled component degrades to a notify, so the report can never claim an
@@ -145,6 +193,7 @@ func cmdUpdate(args []string, stdout, stderr io.Writer) int {
	}

	fmt.Fprint(stdout, renderUpdateReport(res, selected))
	fmt.Fprint(stdout, renderNotes(src.Notes(), selected))
	if len(selected) > 0 {
		fmt.Fprint(stdout, renderApplyGuidance(res, selected, *force))
	}
@@ -199,6 +248,23 @@ func renderUpdateReport(res updater.Result, selected map[string]bool) string {
	return b.String()
}

// renderNotes prints what the release lookups learned beyond the versions (today: a
// PostgreSQL major past its end of life), for the components the selectors show.
func renderNotes(notes map[string]string, selected map[string]bool) string {
	names := make([]string, 0, len(notes))
	for name := range notes {
		if len(selected) == 0 || selectedCovers(selected, name) {
			names = append(names, name)
		}
	}
	sort.Strings(names)
	var b strings.Builder
	for _, name := range names {
		fmt.Fprintf(&b, "%-13s note: %s\n", name, notes[name])
	}
	return b.String()
}

// writeErrs appends one explanatory line per failed component, in a stable order so
// the output does not shuffle between runs, honouring the active selector filter.
func writeErrs(b *strings.Builder, label string, errs map[string]error, selected map[string]bool) {
@@ -234,7 +300,7 @@ func renderApplyGuidance(res updater.Result, selected map[string]bool, force boo
	}

	var b strings.Builder
	var offeredCommand bool
	var offeredInstaller bool
	for _, t := range updateTargets {
		if !selected[t.selector] {
			continue
@@ -262,7 +328,7 @@ func renderApplyGuidance(res updater.Result, selected map[string]bool, force boo
			fmt.Fprintf(&b, "  note: cannot tell whether %s is current — its latest version could not be discovered (see above); this reinstalls it either way\n", t.component)
		}
		fmt.Fprintf(&b, "  run: %s\n", strings.ReplaceAll(t.command, "{ref}", installerRef(byComponent)))
		offeredCommand = true
		offeredInstaller = offeredInstaller || t.installer
	}
	// Only explain the command when one was actually offered; a --mc-only run has
	// nothing to run and the trailer would be a non-sequitur.
@@ -270,13 +336,13 @@ func renderApplyGuidance(res updater.Result, selected map[string]bool, force boo
	// One trailer serves every selector now: setup is not an apply path at all on a
	// completed install (shouldRunHostBootstrapBeforeConfig only enters the host
	// bootstrap while an install marker is missing), so the installer re-run is the one
	// worked path for all three components and there is no per-component exception left
	// worked path for every component Felis installs and there is no per-component exception left
	// to scope. Two caveats stay because following the advice without them bites real
	// hosts: the channel is not persisted anywhere (a bare re-run on a main host quietly
	// moves it onto releases), and the private repo's one-liner needs the read token
	// back in the environment before it can resolve anything.
	if offeredCommand {
		b.WriteString("\nRe-running the installer applies everything above: it fetches the newest version on\nthe channel in effect and re-applies the bundle (release is the default). The channel\nis not persisted, so pass FELIS_VERSION_BOOTSTRAP=dev if this host tracks main. While\nthis repo is private, the one-liner above 404s without a token; the README's install\nsection has the token'd form that works. felis setup is not this path: on a completed\ninstall it opens the config console and installs nothing newer. Restart game servers\nafterwards.\n")
	if offeredInstaller {
		b.WriteString("\nRe-running the installer applies each installer command above: it fetches the newest version on\nthe channel in effect and re-applies the bundle (release is the default). The channel\nis not persisted, so pass FELIS_VERSION_BOOTSTRAP=dev if this host tracks main. While\nthis repo is private, the one-liner above 404s without a token; the README's install\nsection has the token'd form that works. felis setup is not this path: on a completed\ninstall it opens the config console and installs nothing newer. Restart game servers\nafterwards.\n")
	}
	return b.String()
}
@@ -299,10 +365,11 @@ func installerRef(byComponent map[string]updates.Action) string {
}

// isReleaseTag reports whether v was read from a stable vX.Y.Z tag, the only refs
// release.yml publishes a binary for.
// release.yml publishes a binary for. A source build stamps v0.0.0+g<commit>, which
// names no tag, so build metadata disqualifies a version too.
func isReleaseTag(v updates.Version) bool {
	s := v.String()
	if !strings.HasPrefix(s, "v") || v.IsPrerelease() {
	if !strings.HasPrefix(s, "v") || v.IsPrerelease() || strings.Contains(s, "+") {
		return false
	}
	_, err := updates.Parse(s)
+80 −0
Changes for cmd/felis/update_test.go: 80 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -199,3 +199,83 @@ func TestApplyGuidanceReadsTheInstallerAtTheReleaseTag(t *testing.T) {
		}
	}
}

// Every selector in the table is a flag: the FlagSet is built from the table.
func TestUpdateSelectorsAreFlags(t *testing.T) {
	var out, errb strings.Builder
	if code := cmdUpdate([]string{"-h"}, &out, &errb); code != 2 {
		t.Fatalf("-h exit = %d, want 2", code)
	}
	for _, target := range updateTargets {
		if !strings.Contains(errb.String(), "-"+target.selector+"\n") {
			t.Errorf("usage has no -%s flag:\n%s", target.selector, errb.String())
		}
	}
}

// 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.
func TestApplyGuidanceForHostDependencies(t *testing.T) {
	notify := func(c string) updater.Result {
		return planResult([]updates.Action{{Component: c, Kind: updates.ActionNotify, LatestKnown: true}})
	}
	for _, sel := range []string{"k3s", "cloudflared"} {
		out := renderApplyGuidance(notify(sel), map[string]bool{sel: true}, false)
		if !strings.Contains(out, "sudo FELIS_UPGRADE_DEPS=1 bash") || !strings.Contains(out, "Re-running the installer") {
			t.Errorf("--%s guidance must re-run the installer with FELIS_UPGRADE_DEPS=1:\n%s", sel, out)
		}
	}
	jre := renderApplyGuidance(notify("jre"), map[string]bool{"jre": true}, false)
	if !strings.Contains(jre, "| sudo bash") || strings.Contains(jre, "FELIS_UPGRADE_DEPS") {
		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)
	}
}

func TestRenderNotesHonoursSelectors(t *testing.T) {
	notes := map[string]string{"postgresql": "PostgreSQL 13 reached end of life on 2025-11-13"}
	if out := renderNotes(notes, nil); !strings.Contains(out, "postgresql") || !strings.Contains(out, "note: PostgreSQL 13 reached end of life") {
		t.Errorf("unfiltered notes = %q", out)
	}
	if out := renderNotes(notes, map[string]bool{"postgres": true}); !strings.Contains(out, "end of life") {
		t.Errorf("--postgres must show its note, got %q", out)
	}
	if out := renderNotes(notes, map[string]bool{"velocity": true}); out != "" {
		t.Errorf("--velocity must not show the postgresql note, got %q", out)
	}
}

// A source build's v0.0.0+g<commit> names no tag, so the installer one-liner has to
// fall back to main instead of a 404ing ref.
func TestInstallerRefNamesATag(t *testing.T) {
	v := func(s string) updates.Version {
		t.Helper()
		x, err := updates.Parse(s)
		if err != nil {
			t.Fatal(err)
		}
		return x
	}
	cases := []struct {
		name string
		api  updates.Action
		want string
	}{
		{"newest release", updates.Action{Current: v("v1.2.0"), Latest: v("v1.3.0"), LatestKnown: true}, "v1.3.0"},
		{"feed down, host on a release", updates.Action{Current: v("v1.2.0")}, "v1.2.0"},
		{"source build", updates.Action{Current: v("v0.0.0+gunknown")}, "main"},
		{"source build with commit", updates.Action{Current: v("v0.0.0+g1a2b3c4")}, "main"},
		{"prerelease", updates.Action{Current: v("v1.3.0-rc.1")}, "main"},
	}
	for _, c := range cases {
		if got := installerRef(map[string]updates.Action{"felis-api": c.api}); got != c.want {
			t.Errorf("%s: installerRef = %q, want %q", c.name, got, c.want)
		}
	}
	if got := installerRef(nil); got != "main" {
		t.Errorf("no felis-api row: installerRef = %q, want main", got)
	}
}
+85 −15
Changes for deploy/bootstrap.sh: 85 added lines, 15 removed lines.
Original line number Diff line number Diff line
@@ -60,10 +60,13 @@
#   FELIS_GO_SHA256   sha256 of that version's linux tarball for this host's architecture.
#                     REQUIRED for a non-default FELIS_GO_VERSION; the default's is pinned.
#   FELIS_K3S_VERSION k3s release a fresh install gets (default: v1.36.4+k3s1). An
#                     installed k3s is left alone.
#                     installed k3s is left alone unless FELIS_UPGRADE_DEPS=1.
#   FELIS_CLOUDFLARED_VERSION / FELIS_CLOUDFLARED_SHA256 cloudflared release installed
#                     when none is present (default: 2026.9.1, digests pinned); the
#                     sha256 is REQUIRED for any other version
#   FELIS_UPGRADE_DEPS 1 moves an installed k3s and cloudflared to the versions above
#                     (k3s one minor version at a time; neither is ever downgraded) and
#                     restarts cloudflared-felis onto the new binary (default: 0)
#   FELIS_REPO_URL    git URL to build from   (raw script mode only)
#   FELIS_VERSION_BOOTSTRAP release|dev — which version to install (default: release).
#                     release DOWNLOADS the prebuilt felis binary published for the newest
@@ -227,18 +230,20 @@ FELIS_GO_VERSION="${FELIS_GO_VERSION:-$GO_PINNED_VERSION}"
FELIS_GO_SHA256="${FELIS_GO_SHA256:-}"
# cloudflared runs as root on the edge, so it gets the same treatment: a pinned release and
# the sha256 GitHub lists for each asset. A different FELIS_CLOUDFLARED_VERSION has to bring
# its own FELIS_CLOUDFLARED_SHA256. install_cloudflared only runs when the binary is absent;
# upgrading an installed one is `felis update`'s report plus a manual swap.
# its own FELIS_CLOUDFLARED_SHA256. An installed binary is replaced only under
# FELIS_UPGRADE_DEPS=1; `felis update --cloudflared` reports when that would change it.
CLOUDFLARED_PINNED_VERSION="2026.9.1"
CLOUDFLARED_PINNED_SHA256_AMD64="03f1f25d1cc93b9ad6c60569d44060bc4f17ed97075760ed8cfca4b12dcd68cc"
CLOUDFLARED_PINNED_SHA256_ARM64="3d97437c71848bd8df68041e12436b484a661d95073ea1937f01a845ce88faa3"
CLOUDFLARED_PINNED_SHA256_ARM="093ffa3638ab2b636de63c43a8c68f96a69cf71f9699dd8277a91b160b0f4fc0"
FELIS_CLOUDFLARED_VERSION="${FELIS_CLOUDFLARED_VERSION:-$CLOUDFLARED_PINNED_VERSION}"
FELIS_CLOUDFLARED_SHA256="${FELIS_CLOUDFLARED_SHA256:-}"
CLOUDFLARED_BIN=/usr/local/bin/cloudflared
# The k3s release a fresh install gets, and the tag its install script is read from. The
# script checks the k3s binary against that release's sha256sum file, so pinning the tag
# pins both. An installed k3s is never touched; see docs/troubleshooting.md for upgrades.
# pins both. An installed k3s moves only under FELIS_UPGRADE_DEPS=1.
FELIS_K3S_VERSION="${FELIS_K3S_VERSION:-v1.36.4+k3s1}"
FELIS_UPGRADE_DEPS="${FELIS_UPGRADE_DEPS:-0}"
# The in-cluster registry's image, by digest. It must equal platform.defaultRegistryImage
# (internal/platform/identities.go, TestBootstrapPinsTheRegistryImage): the renderer puts
# that ref in the Deployment, and this script caches and pins the same ref in containerd.
@@ -711,6 +716,16 @@ validate_settings() {
  esac
  [ "$(heap_megabytes "$FELIS_VELOCITY_XMX")" -ge 256 ] \
    || die "FELIS_VELOCITY_XMX must be a heap size of at least 256M, written <n>M or <n>G (got '${FELIS_VELOCITY_XMX}')"
  case "$FELIS_UPGRADE_DEPS" in
    0|1) ;;
    *) die "FELIS_UPGRADE_DEPS must be 0 or 1 (got '${FELIS_UPGRADE_DEPS}')" ;;
  esac
}

# version_newer reports whether version $1 sorts after $2 (a leading v is ignored).
version_newer() {
  local a="${1#v}" b="${2#v}"
  [ "$a" != "$b" ] && [ "$(printf '%s\n%s\n' "$a" "$b" | sort -V | tail -n 1)" = "$a" ]
}

# heap_megabytes prints a JVM heap size written <n>M or <n>G in megabytes, or 0 for any
@@ -908,10 +923,26 @@ install_base() {
}

install_cloudflared() {
  if command -v cloudflared >/dev/null 2>&1; then
    ok "cloudflared already installed"
  local current="" path
  if path="$(command -v cloudflared 2>/dev/null)"; then
    current="$(cloudflared --version 2>/dev/null | awk '{ for (i = 1; i < NF; i++) if ($i == "version") { print $(i + 1); exit } }')"
    if [ "$current" = "$FELIS_CLOUDFLARED_VERSION" ]; then
      ok "cloudflared ${current} already installed"
      return 0
    fi
    if [ "$FELIS_UPGRADE_DEPS" != 1 ]; then
      ok "cloudflared ${current:-(version unreadable)} already installed; this release pins ${FELIS_CLOUDFLARED_VERSION} (FELIS_UPGRADE_DEPS=1 moves it)"
      return 0
    fi
    if [ "$path" != "$CLOUDFLARED_BIN" ]; then
      warn "cloudflared at ${path} was not installed by Felis; upgrade it the way it was installed"
      return 0
    fi
    if [ -n "$current" ] && version_newer "$current" "$FELIS_CLOUDFLARED_VERSION"; then
      ok "cloudflared ${current} is newer than the pinned ${FELIS_CLOUDFLARED_VERSION}; left as it is"
      return 0
    fi
  fi
  local machine arch url tmp want have
  machine="$(uname -m)"
  case "$machine" in
@@ -932,9 +963,14 @@ install_cloudflared() {
    rm -f "$tmp"
    die "cloudflared-linux-${arch} ${FELIS_CLOUDFLARED_VERSION} hashes to ${have}, expected ${want}; refusing to install it"
  fi
  install -m 0755 "$tmp" /usr/local/bin/cloudflared
  install -m 0755 "$tmp" "$CLOUDFLARED_BIN"
  rm -f "$tmp"
  ok "cloudflared installed ($(cloudflared --version | head -n 1))"
  # The running tunnel keeps the old binary mapped until it restarts.
  if [ -n "$current" ] && systemctl is-active --quiet cloudflared-felis 2>/dev/null; then
    systemctl restart cloudflared-felis
    ok "cloudflared-felis restarted onto ${FELIS_CLOUDFLARED_VERSION}"
  fi
}

# ---------------------------------------------------------------------------
@@ -1060,16 +1096,19 @@ install_k3s() {
  configure_k3s_firewall

  if [ -x "$K3S_BIN" ]; then
    ok "k3s already installed at ${K3S_BIN}"
    local current
    current="$("$K3S_BIN" --version 2>/dev/null | awk 'NR == 1 { print $3 }')"
    if [ "$current" = "$FELIS_K3S_VERSION" ]; then
      ok "k3s ${current} already installed at ${K3S_BIN}"
    elif [ "$FELIS_UPGRADE_DEPS" != 1 ]; then
      ok "k3s ${current:-(version unreadable)} already installed at ${K3S_BIN}; this release pins ${FELIS_K3S_VERSION} (FELIS_UPGRADE_DEPS=1 moves it)"
    elif k3s_upgrade_allowed "$current" "$FELIS_K3S_VERSION"; then
      log "upgrading k3s ${current} to ${FELIS_K3S_VERSION}; running pods keep running while it restarts"
      run_k3s_installer
    fi
  else
    log "installing k3s ${FELIS_K3S_VERSION} into ${K3S_BIN_DIR} (no traefik/servicelb/metrics-server)"
    # The script from the release's own tag rather than get.k3s.io, which serves whatever
    # master holds today. '+' is literal in a URL path, so the tag needs no escaping.
    curl -sfL --retry 5 --retry-delay 2 "https://raw.githubusercontent.com/k3s-io/k3s/${FELIS_K3S_VERSION}/install.sh" | \
      INSTALL_K3S_VERSION="$FELIS_K3S_VERSION" \
      INSTALL_K3S_BIN_DIR="$K3S_BIN_DIR" \
      INSTALL_K3S_EXEC="--disable traefik --disable servicelb --disable metrics-server --write-kubeconfig-mode 644" \
      sh -
    run_k3s_installer
  fi

  [ -x "$K3S_BIN" ] || die "k3s installation completed but ${K3S_BIN} is missing"
@@ -1080,6 +1119,37 @@ install_k3s() {
  wait_for_node_ready
}

# The script from the release's own tag rather than get.k3s.io, which serves whatever
# master holds today. '+' is literal in a URL path, so the tag needs no escaping. On an
# installed k3s the same script replaces the binary in place and restarts the service.
run_k3s_installer() {
  curl -sfL --retry 5 --retry-delay 2 "https://raw.githubusercontent.com/k3s-io/k3s/${FELIS_K3S_VERSION}/install.sh" | \
    INSTALL_K3S_VERSION="$FELIS_K3S_VERSION" \
    INSTALL_K3S_BIN_DIR="$K3S_BIN_DIR" \
    INSTALL_K3S_EXEC="--disable traefik --disable servicelb --disable metrics-server --write-kubeconfig-mode 644" \
    sh -
}

# k3s_upgrade_allowed decides whether an installed k3s ($1) may move to $2. Kubernetes
# supports upgrading one minor version at a time, so a larger jump stops the install
# before anything changed; a newer installed k3s is left as it is.
k3s_upgrade_allowed() {
  local current="$1" want="$2" cur_major cur_minor want_major want_minor rest
  IFS=. read -r cur_major cur_minor rest <<<"${current#v}"
  IFS=. read -r want_major want_minor rest <<<"${want#v}"
  case "${cur_major}${cur_minor}${want_major}${want_minor}" in
    ""|*[!0-9]*) die "cannot compare the installed k3s '${current}' with ${want}; upgrade it by hand (docs/operations.md §4)" ;;
  esac
  if version_newer "$current" "$want"; then
    ok "k3s ${current} is newer than the pinned ${want}; left as it is"
    return 1
  fi
  if [ "$cur_major" != "$want_major" ] || [ "$((want_minor - cur_minor))" -gt 1 ]; then
    die "k3s ${current} -> ${want} skips a minor version, and Kubernetes upgrades one minor at a time. Rerun with FELIS_K3S_VERSION set to the newest v${cur_major}.$((cur_minor + 1)).x+k3sN release first (https://github.com/k3s-io/k3s/releases)"
  fi
  return 0
}

# Waits for the (single) node to report Ready. Shared by the k3s install and the
# registry-mirror restart below: both restart the agent, and a bootstrap that
# proceeds early fails later with a misleading "not found"/timeout instead.
+65 −4

File changed.

Preview size limit exceeded, changes collapsed.

+28 −5

File changed.

Preview size limit exceeded, changes collapsed.

Loading