From c4adaf831931d2fdbfd007b39fb2d71623b2f70a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85slund?= Date: Sun, 11 Oct 2026 23:39:36 +0000 Subject: [PATCH 1/2] fix(bootstrap): k3s channels go to INSTALL_K3S_CHANNEL; data-dir drop-ins may be *.yml in any case The k3s installer reads INSTALL_K3S_VERSION as an exact release, so stable, latest, testing or v1.34 there would fail. The install library moves a channel name to INSTALL_K3S_CHANNEL and unsets the version before any policy check; tnctl bootstrap --k3s-version renders channels the same way. tn_ca_resolve_datadir and tn_ca_datadir_consistent read config.yaml.d/*.yaml and *.yml, suffix case-insensitive like k3s, in lexical order, last wins. Co-Authored-By: Claude Opus 5.5 --- cmd/bootstrap.go | 2 +- docs/commands/bootstrap.md | 5 +- pkg/templates/bootstrap_templates.go | 22 +-- pkg/templates/k3schannel_test.go | 164 +++++++++++++++++++++ pkg/templates/k3sinstall_test.go | 5 +- pkg/templates/partials/k3s_version.sh.tmpl | 5 +- pkg/templates/scripts/tn-k3s-ca.sh | 10 +- pkg/templates/scripts/tn-k3s-install.sh | 20 ++- pkg/templates/templates_test.go | 17 +-- 9 files changed, 222 insertions(+), 28 deletions(-) create mode 100644 pkg/templates/k3schannel_test.go diff --git a/cmd/bootstrap.go b/cmd/bootstrap.go index 87c6ddb..263af77 100644 --- a/cmd/bootstrap.go +++ b/cmd/bootstrap.go @@ -176,7 +176,7 @@ func generateK3SToken() (string, error) { func init() { bootstrapCmd.Flags().Bool("skip-upload", false, "Skip uploading the generated scripts") bootstrapCmd.Flags().String("k3s-version", templates.DefaultK3SVersion, - "k3s release the scripts install (INSTALL_K3S_VERSION): an exact release like v1.36.5+k3s1, or stable / latest for the installer's channels. Server and agent scripts use the same value") + "k3s release the scripts install (INSTALL_K3S_VERSION): an exact release like v1.36.5+k3s1, or a channel (stable, latest, testing, v1.34, passed as INSTALL_K3S_CHANNEL). Server and agent scripts use the same value") bootstrapCmd.Flags().Bool("k3s-upgrade", false, "let the scripts replace an installed k3s with --k3s-version (TN_K3S_UPGRADE=true is the default in the scripts). Without it a re-run keeps the installed k3s; a downgrade is always refused") bootstrapCmd.Flags().Bool("force", false, diff --git a/docs/commands/bootstrap.md b/docs/commands/bootstrap.md index 3a18c2b..daa69ba 100644 --- a/docs/commands/bootstrap.md +++ b/docs/commands/bootstrap.md @@ -15,7 +15,7 @@ The device may be given as an argument, via the `--device` global flag, or selec | Flag | Default | Description | | --- | --- | --- | | `--skip-upload` | `false` | Skip uploading the generated scripts. | -| `--k3s-version` | `v1.36.5+k3s1` | k3s release the server script installs: an exact release (`v1.36.5+k3s1`), or `stable` / `latest` for the installer's channels. The agent script installs the release of its server instead; a release given here explicitly is installed only if it is not newer than the server's. | +| `--k3s-version` | `v1.36.5+k3s1` | k3s release the server script installs: an exact release (`v1.36.5+k3s1`), or a channel (`stable`, `latest`, `testing`, `v1.34`) for the installer to resolve. The agent script installs the release of its server instead; a release given here explicitly is installed only if it is not newer than the server's. | | `--k3s-upgrade` | `false` | Let the scripts replace an installed k3s with `--k3s-version` (sets `TN_K3S_UPGRADE=true` as the script's default). Without it a re-run **keeps** the installed k3s. See [Re-running a script](#re-running-a-script-the-installed-k3s-is-kept). | | `--force` | `false` | Only with `--k3s-upgrade`: allow an upgrade over more than one minor version (`TN_K3S_FORCE=true`). A downgrade is never allowed. | | `--docker-secret` | `""` | Contents of the `.dockerconfigjson` to embed. | @@ -48,8 +48,7 @@ the day the script runs, and that moved the fleet onto a k3s that refused the de | --- | --- | | *(default)* or `v1.36.5+k3s1` | `INSTALL_K3S_VERSION=v1.36.5+k3s1` | | any other `vX.Y.Z+k3sN` | that release | -| `stable` | nothing: the old behaviour, the installer picks the `stable` channel | -| `latest` | `INSTALL_K3S_CHANNEL=latest` | +| `stable`, `latest`, `testing`, or a minor such as `v1.34` | `INSTALL_K3S_CHANNEL=` (never `INSTALL_K3S_VERSION`: the k3s installer reads that as an exact release). A channel given as `INSTALL_K3S_VERSION=stable` when running the script is moved to `INSTALL_K3S_CHANNEL` the same way. | - At run time, `INSTALL_K3S_VERSION=v1.xx.y+k3sN sh bootstrap_server_.sh` overrides the pin. - The pin is the release for a **new server**. A device that already has k3s keeps what it has, see diff --git a/pkg/templates/bootstrap_templates.go b/pkg/templates/bootstrap_templates.go index 6a5e35c..9a8a8a1 100644 --- a/pkg/templates/bootstrap_templates.go +++ b/pkg/templates/bootstrap_templates.go @@ -16,14 +16,17 @@ const DefaultK3SVersion = "v1.36.5+k3s1" var k3sVersionRE = regexp.MustCompile(`^v[0-9]+\.[0-9]+\.[0-9]+\+k3s[0-9]+$`) +// k3sChannelRE is a k3s release channel: stable, latest, testing, or a minor version such as v1.34. +var k3sChannelRE = regexp.MustCompile(`^(stable|latest|testing|v[0-9]+\.[0-9]+)$`) + // ValidateK3SVersion accepts an exact release (v1.36.5+k3s1), or the channels "stable" (the // installer's own default: whatever stable is when the script runs) and "latest". The value ends up // in a shell script, so nothing else is let through. func ValidateK3SVersion(v string) error { - if v == "" || v == "stable" || v == "latest" || k3sVersionRE.MatchString(v) { + if v == "" || k3sChannelRE.MatchString(v) || k3sVersionRE.MatchString(v) { return nil } - return fmt.Errorf("invalid k3s version %q: want an exact release like %s, or stable or latest", v, DefaultK3SVersion) + return fmt.Errorf("invalid k3s version %q: want an exact release like %s, or a channel (stable, latest, testing, v1.34)", v, DefaultK3SVersion) } type TemplateData struct { @@ -37,7 +40,7 @@ type TemplateData struct { PublicSSHKey string K3SToken string DockerSecret string - // K3SVersion is the k3s release to install: an exact release, "stable" or "latest". + // K3SVersion is the k3s release to install: an exact release, or a channel (stable, latest, testing, v1.34). // Empty means DefaultK3SVersion. K3SVersion string // K3SVersionExplicit is true when K3SVersion was asked for (`tnctl bootstrap --k3s-version`), not @@ -58,19 +61,20 @@ func (d TemplateData) k3sVersion() string { return d.K3SVersion } -// K3SPin is the value for INSTALL_K3S_VERSION, or "" when the installer is to pick (stable, latest). +// K3SPin is the value for INSTALL_K3S_VERSION, or "" when a channel is asked for (see K3SChannel). func (d TemplateData) K3SPin() string { - if v := d.k3sVersion(); v != "stable" && v != "latest" { + if v := d.k3sVersion(); !k3sChannelRE.MatchString(v) { return v } return "" } -// K3SChannel is the value for INSTALL_K3S_CHANNEL: "latest", or "" (the installer's default, -// stable, and the pinned releases). +// K3SChannel is the value for INSTALL_K3S_CHANNEL when --k3s-version names a channel (stable, latest, +// testing, v1.34), and "" for an exact release. The installer reads INSTALL_K3S_VERSION as an exact +// release only, so a channel must never be put there. func (d TemplateData) K3SChannel() string { - if d.k3sVersion() == "latest" { - return "latest" + if v := d.k3sVersion(); k3sChannelRE.MatchString(v) { + return v } return "" } diff --git a/pkg/templates/k3schannel_test.go b/pkg/templates/k3schannel_test.go new file mode 100644 index 0000000..3c03fa4 --- /dev/null +++ b/pkg/templates/k3schannel_test.go @@ -0,0 +1,164 @@ +package templates + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// A channel name is not a release: the k3s installer downloads INSTALL_K3S_VERSION as an exact tag +// (stable would be a 404), and resolves INSTALL_K3S_CHANNEL itself. The library moves a channel that +// arrives as the version to INSTALL_K3S_CHANNEL, and keeps all the other checks. + +func TestChannelAsVersionBecomesInstallK3sChannel(t *testing.T) { + for _, tc := range []struct { + name string + installed string + env []string + wantRC int + want string + }{ + {"stable, fresh", "", []string{"INSTALL_K3S_VERSION=stable"}, 0, "version=unset channel=stable action=install effective="}, + {"latest, fresh", "", []string{"INSTALL_K3S_VERSION=latest"}, 0, "version=unset channel=latest action=install effective="}, + {"testing, fresh", "", []string{"INSTALL_K3S_VERSION=testing"}, 0, "version=unset channel=testing action=install effective="}, + {"v1.34, fresh", "", []string{"INSTALL_K3S_VERSION=v1.34"}, 0, "version=unset channel=v1.34 action=install effective="}, + {"exact stays exact", "", []string{"INSTALL_K3S_VERSION=v1.34.11+k3s1"}, 0, "version=v1.34.11+k3s1 channel=unset action=install effective=v1.34.11+k3s1"}, + {"exact without revision stays exact", "", []string{"INSTALL_K3S_VERSION=v1.34.11"}, 0, "version=v1.34.11 channel=unset action=install effective=v1.34.11"}, + {"a channel in INSTALL_K3S_CHANNEL is left alone", "", []string{"INSTALL_K3S_CHANNEL=stable"}, 0, "version=unset channel=stable action=install effective="}, + {"installed k3s is kept by default", "v1.35.9+k3s1", []string{"INSTALL_K3S_VERSION=stable"}, 0, "version=v1.35.9+k3s1 channel=unset action=keep effective=v1.35.9+k3s1"}, + {"upgrade with a channel is allowed, with a warning", "v1.35.9+k3s1", []string{"INSTALL_K3S_VERSION=stable", "TN_K3S_UPGRADE=true"}, 0, "version=unset channel=stable action=upgrade effective="}, + {"v1.34 channel, upgrade", "v1.33.2+k3s1", []string{"INSTALL_K3S_VERSION=v1.34", "TN_K3S_UPGRADE=true"}, 0, "version=unset channel=v1.34 action=upgrade effective="}, + {"exact downgrade is still refused", "v1.36.5+k3s1", []string{"INSTALL_K3S_VERSION=v1.34.11+k3s1", "TN_K3S_UPGRADE=true"}, 1, ""}, + {"exact multi-minor is still refused", "v1.33.2+k3s1", []string{"INSTALL_K3S_VERSION=v1.36.5+k3s1", "TN_K3S_UPGRADE=true"}, 1, ""}, + {"junk that looks like a channel is left to the installer", "", []string{"INSTALL_K3S_VERSION=v1.34.x"}, 0, "version=v1.34.x channel=unset action=install effective=v1.34.x"}, + } { + t.Run(tc.name, func(t *testing.T) { + i := newInst(t) + i.installed(tc.installed) + out, err := i.run("tn_inst_prepare server; "+resultLine, append([]string{"TN_CGROUP_FSTYPE=cgroup2fs"}, tc.env...)...) + if rc := exitCode(err); rc != tc.wantRC { + t.Fatalf("rc=%d, want %d\n%s", rc, tc.wantRC, out) + } + if tc.wantRC == 0 { + if got := result(t, out); got != tc.want { + t.Errorf("got %s\nwant %s\n%s", got, tc.want, out) + } + } + }) + } +} + +func TestChannelAgentIsNotComparedWithTheServer(t *testing.T) { + i := newInst(t) + i.installed("") + out, err := i.run("tn_inst_prepare agent; "+resultLine, "TN_CGROUP_FSTYPE=cgroup2fs", "K3S_URL="+agentURL, "TN_TEST_PUBLISHED=v1.35.9+k3s1", + "INSTALL_K3S_VERSION=stable", "TN_K3S_PIN_SOURCE=env") + if err != nil { + t.Fatalf("%v\n%s", err, out) + } + // the agent runs the release of its server, and the installer is not given the channel + if got := result(t, out); !strings.HasPrefix(got, "version=v1.35.9+k3s1 channel=unset action=install") { + t.Errorf("got %s\n%s", got, out) + } + if !strings.Contains(out, "not an exact release") { + t.Errorf("no note that the channel cannot be compared:\n%s", out) + } +} + +// A fake k3s installer: it records what it receives, and fails like the real one when +// INSTALL_K3S_VERSION is not an exact release. +func TestInstallerReceivesChannelOrVersionNeverBoth(t *testing.T) { + for _, tc := range []struct { + env []string + wantVersion string + wantChannel string + }{ + {[]string{"INSTALL_K3S_VERSION=stable"}, "", "stable"}, + {[]string{"INSTALL_K3S_VERSION=v1.34"}, "", "v1.34"}, + {[]string{"INSTALL_K3S_VERSION=latest", "INSTALL_K3S_CHANNEL=stable"}, "", "latest"}, + {[]string{"INSTALL_K3S_VERSION=v1.36.5+k3s1"}, "v1.36.5+k3s1", ""}, + } { + i := newInst(t) + i.installed("") + rec := filepath.Join(i.dir, "installer.rec") + inst := filepath.Join(i.dir, "k3s_installer.sh") + write(t, inst, "#!/bin/sh\necho \"version=${INSTALL_K3S_VERSION-unset} channel=${INSTALL_K3S_CHANNEL-unset}\" > '"+rec+"'\n"+ + "case ${INSTALL_K3S_VERSION-} in stable|latest|testing|v[0-9]*.[0-9]) echo 'fake installer: not a release' >&2; exit 22 ;; esac\n") + if err := os.Chmod(inst, 0o755); err != nil { + t.Fatal(err) + } + out, err := i.run("tn_inst_prepare server; "+inst, append([]string{"TN_CGROUP_FSTYPE=cgroup2fs"}, tc.env...)...) + if err != nil { + t.Fatalf("%v: %v\n%s", tc.env, err, out) + } + got := strings.TrimSpace(string(mustRead(t, rec))) + v, c := "unset", "unset" + if tc.wantVersion != "" { + v = tc.wantVersion + } + if tc.wantChannel != "" { + c = tc.wantChannel + } + if want := "version=" + v + " channel=" + c; got != want { + t.Errorf("%v: installer got %q, want %q", tc.env, got, want) + } + } +} + +func TestChannelCodeRunsUnderDash(t *testing.T) { + needTool(t, "dash") + i := newInst(t) + i.installed("") + cmd := exec.Command("dash", "-c", "set -e; . "+installLibPath+"; INSTALL_K3S_VERSION=stable; export INSTALL_K3S_VERSION; tn_inst_normalize_pin; "+resultLine) + cmd.Env = append(os.Environ(), "TN_K3S_CONFIG_DIR="+i.k3sCfg) + out, err := cmd.CombinedOutput() + if err != nil || !strings.Contains(string(out), "RESULT version=unset channel=stable") { + t.Errorf("%v\n%s", err, out) + } +} + +// k3s reads config.yaml.d/*.yaml and *.yml, the suffix without regard to case, in lexical order, and +// the last file that sets a key wins (pkg/configfilearg: HasSuffixI). +func TestDataDirFromDropinsOfEverySuffix(t *testing.T) { + for _, tc := range []struct { + name string + files map[string]string // name -> data-dir + want string // file that wins, "" = default + }{ + {"a .yml drop-in", map[string]string{"50-x.yml": "A"}, "50-x.yml"}, + {"a .YAML drop-in", map[string]string{"50-x.YAML": "A"}, "50-x.YAML"}, + {"a .Yml drop-in", map[string]string{"50-x.Yml": "A"}, "50-x.Yml"}, + {"the last in lexical order wins, across suffixes", map[string]string{"10-a.yaml": "A", "20-b.yml": "B", "30-c.YAML": "C"}, "30-c.YAML"}, + {"a later .yaml beats an earlier .yml", map[string]string{"10-a.yml": "A", "20-b.yaml": "B"}, "20-b.yaml"}, + {"other suffixes are ignored", map[string]string{"10-a.yaml": "A", "20-b.txt": "B", "30-c.yaml.bak": "C"}, "10-a.yaml"}, + } { + t.Run(tc.name, func(t *testing.T) { + p := newPKI(t) + p.legacyCert() + write(t, p.unit, unitText()) + dirs := map[string]string{} + for f, v := range tc.files { + dirs[f] = filepath.Join(p.dir, "dd-"+v) + write(t, filepath.Join(p.k3sCfg, "config.yaml.d", f), "data-dir: "+dirs[f]+"\n") + } + out, err := p.sh(`tn_ca_resolve_datadir; echo "DD=$DATA_DIR SRC=$TN_CA_DATADIR_SRC"; if tn_ca_datadir_consistent; then echo CONSISTENT; else echo "INCONSISTENT $TN_CA_CFG_DATADIR_SRC"; fi`, "DATA_DIR=") + if err != nil { + t.Fatalf("%v\n%s", err, out) + } + wantDir := dirs[tc.want] + if want := "DD=" + wantDir + " SRC=" + filepath.Join(p.k3sCfg, "config.yaml.d", tc.want); !strings.Contains(out, want) { + t.Errorf("want %q in\n%s", want, out) + } + if !strings.Contains("\n"+out, "\nCONSISTENT") { + t.Errorf("the resolved data dir disagrees with the files:\n%s", out) + } + // a different data dir, from outside the files, is inconsistent with them + out, _ = p.sh(`DATA_DIR=/elsewhere; tn_ca_datadir_consistent && echo CONSISTENT || echo "INCONSISTENT $TN_CA_CFG_DATADIR_SRC"`, "DATA_DIR=") + if !strings.Contains(out, "INCONSISTENT "+filepath.Join(p.k3sCfg, "config.yaml.d", tc.want)) { + t.Errorf("%s", out) + } + }) + } +} diff --git a/pkg/templates/k3sinstall_test.go b/pkg/templates/k3sinstall_test.go index 21a3970..ba19fa7 100644 --- a/pkg/templates/k3sinstall_test.go +++ b/pkg/templates/k3sinstall_test.go @@ -856,7 +856,10 @@ func TestRenderedPinThroughThePolicy(t *testing.T) { {"env opts in", TemplateData{}, "v1.35.9+k3s1", []string{"TN_K3S_UPGRADE=true"}, 0, "version=v1.36.5+k3s1 channel=unset action=upgrade"}, {"env pin beats the template pin on a new device", TemplateData{}, "", []string{"INSTALL_K3S_VERSION=v1.34.11+k3s1"}, 0, "version=v1.34.11+k3s1 channel=unset action=install"}, {"latest channel, installed", TemplateData{K3SVersion: "latest"}, "v1.35.9+k3s1", nil, 0, "version=v1.35.9+k3s1 channel=unset action=keep"}, - {"stable, fresh", TemplateData{K3SVersion: "stable"}, "", nil, 0, "version=unset channel=unset action=install"}, + {"stable, fresh", TemplateData{K3SVersion: "stable"}, "", nil, 0, "version=unset channel=stable action=install"}, + {"v1.34 channel, fresh", TemplateData{K3SVersion: "v1.34"}, "", nil, 0, "version=unset channel=v1.34 action=install"}, + {"stable, installed, kept", TemplateData{K3SVersion: "stable"}, "v1.35.9+k3s1", nil, 0, "version=v1.35.9+k3s1 channel=unset action=keep"}, + {"channel in INSTALL_K3S_VERSION at run time", TemplateData{}, "", []string{"INSTALL_K3S_VERSION=stable"}, 0, "version=unset channel=stable action=install"}, {"latest, fresh", TemplateData{K3SVersion: "latest"}, "", nil, 0, "version=unset channel=latest action=install"}, } { t.Run(tc.name, func(t *testing.T) { diff --git a/pkg/templates/partials/k3s_version.sh.tmpl b/pkg/templates/partials/k3s_version.sh.tmpl index 97469b8..ecbb2d4 100644 --- a/pkg/templates/partials/k3s_version.sh.tmpl +++ b/pkg/templates/partials/k3s_version.sh.tmpl @@ -9,10 +9,11 @@ export TN_K3S_PIN_SOURCE # This is the release for a NEW device: a re-run on a device with k3s installed keeps the installed version # (TN_K3S_UPGRADE=true, tnctl bootstrap --k3s-upgrade, replaces it; never a downgrade). export INSTALL_K3S_VERSION="${INSTALL_K3S_VERSION:-{{.K3SPin}}}" -{{else if .K3SChannel}}# k3s follows the {{.K3SChannel}} channel (tnctl bootstrap --k3s-version {{.K3SChannel}}). Pin a release with: +{{else if .K3SChannel}}# k3s follows the {{.K3SChannel}} channel (tnctl bootstrap --k3s-version {{.K3SChannel}}); the installer gets it as +# INSTALL_K3S_CHANNEL (INSTALL_K3S_VERSION takes an exact release only). Pin a release with: # INSTALL_K3S_VERSION=v1.xx.y+k3sN sh bootstrap_*.sh export INSTALL_K3S_CHANNEL="${INSTALL_K3S_CHANNEL:-{{.K3SChannel}}}" -{{else}}# k3s follows the installer's default channel, stable (tnctl bootstrap --k3s-version stable). Pin a release with: +{{else}}# k3s follows the installer's default channel, stable. Pin a release with: # INSTALL_K3S_VERSION=v1.xx.y+k3sN sh bootstrap_*.sh {{end -}} {{if .K3SUpgrade}}# tnctl bootstrap --k3s-upgrade: the release above replaces an installed k3s (never a downgrade). TN_K3S_UPGRADE=false overrides. diff --git a/pkg/templates/scripts/tn-k3s-ca.sh b/pkg/templates/scripts/tn-k3s-ca.sh index 0b316e3..5ea7d68 100644 --- a/pkg/templates/scripts/tn-k3s-ca.sh +++ b/pkg/templates/scripts/tn-k3s-ca.sh @@ -242,6 +242,12 @@ tn_ca_unit_env_datadir() { [ -z "$_tn_ev" ] || echo "$_tn_ev" } +# tn_ca_config_dropins DIR: the drop-ins k3s reads from DIR, in the order it reads them (the last one wins): +# *.yaml and *.yml, the suffix without regard to case (k3s: HasSuffixI in pkg/configfilearg), by name. +tn_ca_config_dropins() { + find "$1" -maxdepth 1 -type f \( -iname '*.yaml' -o -iname '*.yml' \) 2>/dev/null | LC_ALL=C sort +} + tn_ca_resolve_datadir() { _tn_pr=${PRODUCT:-k3s} if [ -n "${DATA_DIR:-}" ]; then @@ -253,7 +259,7 @@ tn_ca_resolve_datadir() { _tn_v=$(tn_ca_unit_datadir) if [ -n "$_tn_v" ]; then DATA_DIR=$_tn_v; TN_CA_DATADIR_SRC="ExecStart of $(tn_ca_unit_name)"; fi if [ -z "$_tn_v" ] && [ -d "$_tn_cd/config.yaml.d" ]; then - for _tn_f in $(find "$_tn_cd/config.yaml.d" -maxdepth 1 -type f -name '*.yaml' 2>/dev/null | LC_ALL=C sort); do + for _tn_f in $(tn_ca_config_dropins "$_tn_cd/config.yaml.d"); do _tn_x=$(tn_ca_yaml_datadir "$_tn_f") if [ -n "$_tn_x" ]; then _tn_v=$_tn_x; TN_CA_DATADIR_SRC=$_tn_f; fi done @@ -282,7 +288,7 @@ tn_ca_datadir_consistent() { TN_CA_CFG_DATADIR=; TN_CA_CFG_DATADIR_SRC= _tn_cd=${TN_K3S_CONFIG_DIR:-/etc/rancher/${PRODUCT:-k3s}} if [ -d "$_tn_cd/config.yaml.d" ]; then - for _tn_f in $(find "$_tn_cd/config.yaml.d" -maxdepth 1 -type f -name '*.yaml' 2>/dev/null | LC_ALL=C sort); do + for _tn_f in $(tn_ca_config_dropins "$_tn_cd/config.yaml.d"); do _tn_x=$(tn_ca_yaml_datadir "$_tn_f") if [ -n "$_tn_x" ]; then TN_CA_CFG_DATADIR=$_tn_x; TN_CA_CFG_DATADIR_SRC=$_tn_f; fi done diff --git a/pkg/templates/scripts/tn-k3s-install.sh b/pkg/templates/scripts/tn-k3s-install.sh index 6efea7f..a4ec5e3 100644 --- a/pkg/templates/scripts/tn-k3s-install.sh +++ b/pkg/templates/scripts/tn-k3s-install.sh @@ -325,11 +325,29 @@ tn_inst_agent_target() { tn_inst_fatal "cannot read the k3s release of the server at ${K3S_URL:-(K3S_URL is not set)} (${K3S_URL:-https://:6443}/static/teknoir/k3s-version): an agent must not run a newer k3s than its server, so this script does not guess. $_tn_why Or say which release the server runs: INSTALL_K3S_VERSION=v1.xx.y+k3sN (tnctl bootstrap --k3s-version), or set TN_K3S_FORCE=true to install ${_tn_pin:-the release the installer picks} anyway. $(tn_inst_state_note)" } +# tn_inst_normalize_pin: the k3s installer takes INSTALL_K3S_VERSION as an EXACT release (it downloads that +# tag, so "stable" would be a 404) and INSTALL_K3S_CHANNEL as a channel. A channel name given as the version +# (stable, latest, testing, or a minor such as v1.34) is moved to INSTALL_K3S_CHANNEL and the version is +# unset, so that every later check sees what the installer will see. An exact release is not touched. +tn_inst_normalize_pin() { + [ -n "${INSTALL_K3S_VERSION:-}" ] || return 0 + case $INSTALL_K3S_VERSION in + stable|latest|testing) ;; + v[0-9]*) printf '%s\n' "$INSTALL_K3S_VERSION" | grep -Eq '^v[0-9]+\.[0-9]+$' || return 0 ;; + *) return 0 ;; + esac + INSTALL_K3S_CHANNEL=$INSTALL_K3S_VERSION + export INSTALL_K3S_CHANNEL + unset INSTALL_K3S_VERSION + tn_inst_info "$INSTALL_K3S_CHANNEL is a k3s channel, not a release: the installer gets INSTALL_K3S_CHANNEL=$INSTALL_K3S_CHANNEL" +} + # tn_inst_plan: applies the policy at the top. Reads INSTALL_K3S_VERSION / INSTALL_K3S_CHANNEL, # sets and exports them for the installer. Sets TN_INST_INSTALLED, TN_INST_ACTION # (install|same|keep|upgrade) and TN_INST_EFFECTIVE (the exact release that will run, or nothing # when only a channel is known). Exits through tn_inst_fatal when it refuses. tn_inst_plan() { + tn_inst_normalize_pin tn_inst_installed_version if [ -z "$TN_INST_INSTALLED" ] && tn_inst_k3s_present; then if [ "${TN_K3S_FORCE:-}" = true ]; then @@ -429,7 +447,7 @@ tn_inst_cgroup_flag_in_text() { tn_inst_cgroup_configured_elsewhere() { _tn_cd=${TN_K3S_CONFIG_DIR:-/etc/rancher/k3s} TN_INST_CGROUP_WHERE=; TN_INST_CGROUP_PERSISTENT=0 - for _tn_cf in "$_tn_cd/config.yaml" $(find "$_tn_cd/config.yaml.d" -maxdepth 1 -type f \( -name '*.yaml' -o -name '*.yml' \) 2>/dev/null | LC_ALL=C sort); do + for _tn_cf in "$_tn_cd/config.yaml" $(find "$_tn_cd/config.yaml.d" -maxdepth 1 -type f \( -iname '*.yaml' -o -iname '*.yml' \) 2>/dev/null | LC_ALL=C sort); do [ -r "$_tn_cf" ] || continue [ "$_tn_cf" != "$_tn_cd/config.yaml.d/$TN_INST_CGROUP_FILE" ] || continue if tn_inst_cgroup_flag_in_text < "$_tn_cf"; then TN_INST_CGROUP_WHERE=$_tn_cf; TN_INST_CGROUP_PERSISTENT=1; return 0; fi diff --git a/pkg/templates/templates_test.go b/pkg/templates/templates_test.go index a05c289..c1254b2 100644 --- a/pkg/templates/templates_test.go +++ b/pkg/templates/templates_test.go @@ -113,13 +113,12 @@ func TestK3SPinOverridesAndChannels(t *testing.T) { if got := exportsOf(out, "INSTALL_K3S_VERSION"); len(got) != 1 || !strings.Contains(got[0], "v1.34.11+k3s1") || strings.Contains(strings.Join(exportsOf(out, "INSTALL_K3S_VERSION"), ""), "v1.36.5") { t.Errorf("%s override: %q", name, got) } - // stable keeps the old behaviour: nothing is exported - out = renderK3S(t, tpl, "stable") - if len(exportsOf(out, "INSTALL_K3S_VERSION"))+len(exportsOf(out, "INSTALL_K3S_CHANNEL")) != 0 { - t.Errorf("%s stable exports a version or channel", name) - } - if !strings.Contains(out, "stable") { - t.Errorf("%s stable: no note", name) + // channels are exported as INSTALL_K3S_CHANNEL, never as INSTALL_K3S_VERSION + for _, ch := range []string{"stable", "testing", "v1.34"} { + out = renderK3S(t, tpl, ch) + if got := exportsOf(out, "INSTALL_K3S_CHANNEL"); len(got) != 1 || !strings.Contains(got[0], ch) || len(exportsOf(out, "INSTALL_K3S_VERSION")) != 0 { + t.Errorf("%s %s: %q", name, ch, got) + } } out = renderK3S(t, tpl, "latest") if got := exportsOf(out, "INSTALL_K3S_CHANNEL"); len(got) != 1 || !strings.Contains(got[0], "latest") || len(exportsOf(out, "INSTALL_K3S_VERSION")) != 0 { @@ -140,12 +139,12 @@ func TestK3SPinIsTheSameForServerAndAgent(t *testing.T) { } func TestValidateK3SVersion(t *testing.T) { - for _, ok := range []string{"", "stable", "latest", "v1.36.5+k3s1", "v1.34.11+k3s2", "v1.27.0+k3s10"} { + for _, ok := range []string{"", "stable", "latest", "v1.36.5+k3s1", "v1.34.11+k3s2", "v1.27.0+k3s10", "testing", "v1.34"} { if err := ValidateK3SVersion(ok); err != nil { t.Errorf("%q: %v", ok, err) } } - for _, bad := range []string{"1.36.5+k3s1", "v1.36.5", "v1.36", "v1.36.5-rc1+k3s1", "testing", "v1.36.5+k3s1; rm -rf /", "$(id)", "v1.36.5+k3s1\n", "`x`", "\"", " stable"} { + for _, bad := range []string{"1.36.5+k3s1", "v1.36.5", "v1.36.", "v1.36.5-rc1+k3s1", "v1.34\n", "Stable", "v1.36.5+k3s1; rm -rf /", "$(id)", "v1.36.5+k3s1\n", "`x`", "\"", " stable"} { if err := ValidateK3SVersion(bad); err == nil { t.Errorf("%q was accepted", bad) } From 8ba02da66237adc2592fd93c1ea5631d619e178d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20=C3=85slund?= Date: Sun, 11 Oct 2026 23:50:56 +0000 Subject: [PATCH 2/2] fix(bootstrap): k3s upgrade checks apply to channels too; a channel in INSTALL_K3S_VERSION never overwrites INSTALL_K3S_CHANNEL With TN_K3S_UPGRADE=true a vMAJ.MIN channel skipped the downgrade and multi-minor refusals. Now a minor channel is compared by major.minor (lower or other major: refused; more than +1: needs TN_K3S_FORCE), stable/latest/testing is resolved at update.k3s.io and checked as an exact release, and an unresolvable channel is refused unless TN_K3S_FORCE=true. Co-Authored-By: Claude Opus 5.5 --- pkg/templates/k3schannel_test.go | 13 ++++++- pkg/templates/k3sinstall_test.go | 43 ++++++++++++++++----- pkg/templates/scripts/tn-k3s-install.sh | 50 ++++++++++++++++++++++--- 3 files changed, 90 insertions(+), 16 deletions(-) diff --git a/pkg/templates/k3schannel_test.go b/pkg/templates/k3schannel_test.go index 3c03fa4..acddfc1 100644 --- a/pkg/templates/k3schannel_test.go +++ b/pkg/templates/k3schannel_test.go @@ -28,7 +28,7 @@ func TestChannelAsVersionBecomesInstallK3sChannel(t *testing.T) { {"exact without revision stays exact", "", []string{"INSTALL_K3S_VERSION=v1.34.11"}, 0, "version=v1.34.11 channel=unset action=install effective=v1.34.11"}, {"a channel in INSTALL_K3S_CHANNEL is left alone", "", []string{"INSTALL_K3S_CHANNEL=stable"}, 0, "version=unset channel=stable action=install effective="}, {"installed k3s is kept by default", "v1.35.9+k3s1", []string{"INSTALL_K3S_VERSION=stable"}, 0, "version=v1.35.9+k3s1 channel=unset action=keep effective=v1.35.9+k3s1"}, - {"upgrade with a channel is allowed, with a warning", "v1.35.9+k3s1", []string{"INSTALL_K3S_VERSION=stable", "TN_K3S_UPGRADE=true"}, 0, "version=unset channel=stable action=upgrade effective="}, + {"upgrade with a channel is resolved and pinned", "v1.35.9+k3s1", []string{"INSTALL_K3S_VERSION=stable", "TN_K3S_UPGRADE=true", "TN_TEST_CHANNEL_RELEASE=v1.36.5%2Bk3s1"}, 0, "version=v1.36.5+k3s1 channel=unset action=upgrade effective=v1.36.5+k3s1"}, {"v1.34 channel, upgrade", "v1.33.2+k3s1", []string{"INSTALL_K3S_VERSION=v1.34", "TN_K3S_UPGRADE=true"}, 0, "version=unset channel=v1.34 action=upgrade effective="}, {"exact downgrade is still refused", "v1.36.5+k3s1", []string{"INSTALL_K3S_VERSION=v1.34.11+k3s1", "TN_K3S_UPGRADE=true"}, 1, ""}, {"exact multi-minor is still refused", "v1.33.2+k3s1", []string{"INSTALL_K3S_VERSION=v1.36.5+k3s1", "TN_K3S_UPGRADE=true"}, 1, ""}, @@ -77,7 +77,7 @@ func TestInstallerReceivesChannelOrVersionNeverBoth(t *testing.T) { }{ {[]string{"INSTALL_K3S_VERSION=stable"}, "", "stable"}, {[]string{"INSTALL_K3S_VERSION=v1.34"}, "", "v1.34"}, - {[]string{"INSTALL_K3S_VERSION=latest", "INSTALL_K3S_CHANNEL=stable"}, "", "latest"}, + {[]string{"INSTALL_K3S_VERSION=latest", "INSTALL_K3S_CHANNEL=latest"}, "", "latest"}, {[]string{"INSTALL_K3S_VERSION=v1.36.5+k3s1"}, "v1.36.5+k3s1", ""}, } { i := newInst(t) @@ -107,6 +107,15 @@ func TestInstallerReceivesChannelOrVersionNeverBoth(t *testing.T) { } } +func TestChannelInVersionMustNotOverwriteAnotherChannel(t *testing.T) { + i := newInst(t) + i.installed("") + out, err := i.run("tn_inst_prepare server", "TN_CGROUP_FSTYPE=cgroup2fs", "INSTALL_K3S_VERSION=latest", "INSTALL_K3S_CHANNEL=stable") + if err == nil || !strings.Contains(out, "conflicting k3s requests") { + t.Fatalf("want a refusal: %v\n%s", err, out) + } +} + func TestChannelCodeRunsUnderDash(t *testing.T) { needTool(t, "dash") i := newInst(t) diff --git a/pkg/templates/k3sinstall_test.go b/pkg/templates/k3sinstall_test.go index ba19fa7..c1d1794 100644 --- a/pkg/templates/k3sinstall_test.go +++ b/pkg/templates/k3sinstall_test.go @@ -45,6 +45,10 @@ for a in "$@"; do url=$a; done if [ -n "$TN_TEST_SERVER_DOWN" ]; then exit 7; fi n=$(wc -l < '`+i.curlLog+`'); if [ "$n" -le "${TN_TEST_FAIL_N:-0}" ]; then exit 7; fi case "$url" in + https://update.k3s.io/v1-release/channels/*) + # TN_TEST_CHANNEL_RELEASE: where the channel redirects to (curl -w '%{url_effective}'); otherwise unreachable + if [ -n "$TN_TEST_CHANNEL_RELEASE" ]; then printf 'https://github.com/k3s-io/k3s/releases/tag/%s' "$TN_TEST_CHANNEL_RELEASE"; exit 0; fi + exit 6 ;; */static/teknoir/k3s-version) if [ -n "$TN_TEST_PUBLISHED" ]; then printf '%s\n' "$TN_TEST_PUBLISHED"; exit 0; fi exit 22 ;; @@ -252,8 +256,29 @@ func TestReleasePolicy(t *testing.T) { msgs: []string{"refusing to downgrade"}}, {name: "a lower patch is a downgrade", installed: "v1.36.6+k3s1", env: []string{"TN_K3S_UPGRADE=true"}, wantRC: 1, msgs: []string{"refusing to downgrade"}}, {name: "a lower k3s revision is a downgrade", installed: "v1.36.5+k3s2", env: []string{"TN_K3S_UPGRADE=true"}, wantRC: 1, msgs: []string{"refusing to downgrade"}}, - {name: "upgrade to a channel cannot be checked", installed: "v1.34.11+k3s1", pin: "-", env: []string{"TN_K3S_UPGRADE=true"}, - want: "version=unset channel=unset action=upgrade effective=", msgs: []string{"not an exact release", "cannot be checked"}}, + {name: "upgrade to the default channel resolving to the pin", installed: "v1.35.9+k3s1", pin: "-", env: []string{"TN_K3S_UPGRADE=true", "TN_TEST_CHANNEL_RELEASE=" + pin}, + want: "version=" + pin + " channel=unset action=upgrade effective=" + pin, msgs: []string{"resolves to " + pin}}, + {name: "channel stable resolving two minors up is refused", installed: "v1.34.11+k3s1", pin: "-", env: []string{"TN_K3S_UPGRADE=true", "INSTALL_K3S_CHANNEL=stable", "TN_TEST_CHANNEL_RELEASE=v1.36.5%2Bk3s1"}, wantRC: 1, + msgs: []string{"more than one minor version", "TN_K3S_FORCE=true"}}, + {name: "channel stable resolving two minors up, with force", installed: "v1.34.11+k3s1", pin: "-", env: []string{"TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true", "INSTALL_K3S_CHANNEL=stable", "TN_TEST_CHANNEL_RELEASE=v1.36.5%2Bk3s1"}, + want: "version=" + pin + " channel=unset action=upgrade effective=" + pin, msgs: []string{"skips minor versions"}}, + {name: "channel latest resolving below the installed release is refused, also with force", installed: "v1.37.1+k3s1", pin: "-", env: []string{"TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true", "INSTALL_K3S_CHANNEL=latest", "TN_TEST_CHANNEL_RELEASE=" + pin}, wantRC: 1, + msgs: []string{"refusing to downgrade"}}, + {name: "unresolvable channel is refused", installed: "v1.34.11+k3s1", pin: "-", env: []string{"TN_K3S_UPGRADE=true", "INSTALL_K3S_CHANNEL=stable"}, wantRC: 1, + msgs: []string{"cannot be resolved", "TN_K3S_FORCE=true", "Nothing was changed"}}, + {name: "unresolvable channel with force goes on unchecked", installed: "v1.34.11+k3s1", pin: "-", env: []string{"TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true", "INSTALL_K3S_CHANNEL=stable"}, + want: "version=unset channel=stable action=upgrade effective=", msgs: []string{"cannot be resolved", "without those checks"}}, + {name: "minor channel one lower is refused", installed: "v1.34.11+k3s1", pin: "v1.33", env: []string{"TN_K3S_UPGRADE=true"}, wantRC: 1, msgs: []string{"refusing to downgrade", "channel v1.33"}}, + {name: "minor channel one lower is refused, also with force", installed: "v1.34.11+k3s1", pin: "v1.33", env: []string{"TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true"}, wantRC: 1, msgs: []string{"refusing to downgrade"}}, + {name: "minor channel of another major is refused", installed: "v1.34.11+k3s1", pin: "v2.0", env: []string{"TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true"}, wantRC: 1, msgs: []string{"refusing to downgrade"}}, + {name: "minor channel two up is refused without force", installed: "v1.34.11+k3s1", pin: "v1.36", env: []string{"TN_K3S_UPGRADE=true"}, wantRC: 1, msgs: []string{"more than one minor version", "v1.34 -> v1.36"}}, + {name: "minor channel two up with force warns", installed: "v1.34.11+k3s1", pin: "v1.36", env: []string{"TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true"}, + want: "version=unset channel=v1.36 action=upgrade effective=", msgs: []string{"skips minor versions"}}, + {name: "minor channel one up is allowed", installed: "v1.34.11+k3s1", pin: "v1.35", env: []string{"TN_K3S_UPGRADE=true"}, + want: "version=unset channel=v1.35 action=upgrade effective="}, + {name: "minor channel of the same minor is allowed", installed: "v1.34.11+k3s1", pin: "v1.34", env: []string{"TN_K3S_UPGRADE=true"}, + want: "version=unset channel=v1.34 action=upgrade effective="}, + {name: "a channel in the version conflicts with another channel", installed: "v1.34.11+k3s1", pin: "v1.35", env: []string{"TN_K3S_UPGRADE=true", "INSTALL_K3S_CHANNEL=stable"}, wantRC: 1, msgs: []string{"conflicting k3s requests"}}, {name: "upgrade of a fresh device is just an install", installed: "", env: []string{"TN_K3S_UPGRADE=true"}, want: "version=" + pin + " channel=unset action=install effective=" + pin}, {name: "TN_K3S_UPGRADE=false is a keep", installed: "v1.34.11+k3s1", env: []string{"TN_K3S_UPGRADE=false"}, @@ -580,9 +605,9 @@ func TestCgroupV1FlagIsOnTheInstallerCommandWhenTheDropinHasToWait(t *testing.T) wantExec string // the whole INSTALL_K3S_EXEC after tn_inst_prepare msg string }{ - {name: "channel upgrade", env: []string{"INSTALL_K3S_CHANNEL=stable", "TN_K3S_UPGRADE=true", "INSTALL_K3S_EXEC=server --tls-san a"}, + {name: "channel upgrade", env: []string{"INSTALL_K3S_CHANNEL=stable", "TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true", "INSTALL_K3S_EXEC=server --tls-san a"}, wantExec: "server --tls-san a " + flag, msg: "may need the flag on its first start"}, - {name: "channel upgrade, no exec at all", env: []string{"INSTALL_K3S_CHANNEL=latest", "TN_K3S_UPGRADE=true"}, + {name: "channel upgrade, no exec at all", env: []string{"INSTALL_K3S_CHANNEL=latest", "TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true"}, wantExec: flag}, {name: "a forced jump to v1.36.5", env: []string{"INSTALL_K3S_VERSION=v1.36.5+k3s1", "TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true", "INSTALL_K3S_EXEC=agent"}, wantExec: "agent " + flag}, @@ -592,11 +617,11 @@ func TestCgroupV1FlagIsOnTheInstallerCommandWhenTheDropinHasToWait(t *testing.T) wantExec: "server"}, {name: "a channel named v1.31 does not need it either", env: []string{"INSTALL_K3S_CHANNEL=v1.31", "TN_K3S_UPGRADE=true", "INSTALL_K3S_EXEC=server"}, wantExec: "server"}, - {name: "already on the command of the caller: not twice", env: []string{"INSTALL_K3S_CHANNEL=stable", "TN_K3S_UPGRADE=true", "INSTALL_K3S_EXEC=server " + flag}, + {name: "already on the command of the caller: not twice", env: []string{"INSTALL_K3S_CHANNEL=stable", "TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true", "INSTALL_K3S_EXEC=server " + flag}, wantExec: "server " + flag, msg: "already has fail-cgroupv1=false"}, - {name: "already on the command of the caller, spelled with a space", env: []string{"INSTALL_K3S_CHANNEL=stable", "TN_K3S_UPGRADE=true", "INSTALL_K3S_EXEC=server --kubelet-arg fail-cgroupv1=false"}, + {name: "already on the command of the caller, spelled with a space", env: []string{"INSTALL_K3S_CHANNEL=stable", "TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true", "INSTALL_K3S_EXEC=server --kubelet-arg fail-cgroupv1=false"}, wantExec: "server --kubelet-arg fail-cgroupv1=false"}, - {name: "true on the command of the caller is not the flag", env: []string{"INSTALL_K3S_CHANNEL=stable", "TN_K3S_UPGRADE=true", "INSTALL_K3S_EXEC=server --kubelet-arg=fail-cgroupv1=true"}, + {name: "true on the command of the caller is not the flag", env: []string{"INSTALL_K3S_CHANNEL=stable", "TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true", "INSTALL_K3S_EXEC=server --kubelet-arg=fail-cgroupv1=true"}, wantExec: "server --kubelet-arg=fail-cgroupv1=true " + flag}, } { t.Run(tc.name, func(t *testing.T) { @@ -636,7 +661,7 @@ func TestCgroupV1FlagIsOnTheInstallerCommandWhenTheDropinHasToWait(t *testing.T) // cgroup v2: nothing at all i = newInst(t) i.installed("v1.30.14+k3s1") - out, err = i.run("tn_inst_prepare server"+show, "TN_CGROUP_FSTYPE=cgroup2fs", "INSTALL_K3S_CHANNEL=stable", "TN_K3S_UPGRADE=true", "INSTALL_K3S_EXEC=server") + out, err = i.run("tn_inst_prepare server"+show, "TN_CGROUP_FSTYPE=cgroup2fs", "INSTALL_K3S_CHANNEL=stable", "TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true", "INSTALL_K3S_EXEC=server") if err != nil || !strings.Contains(out, "EXEC=[server]\n") || !strings.Contains(out, "DROPIN=no") { t.Errorf("%v\n%s", err, out) } @@ -644,7 +669,7 @@ func TestCgroupV1FlagIsOnTheInstallerCommandWhenTheDropinHasToWait(t *testing.T) i = newInst(t) i.installed("v1.30.14+k3s1") write(t, filepath.Join(i.k3sCfg, "config.yaml.d", "10-user.yaml"), "kubelet-arg+:\n - fail-cgroupv1=false\n") - out, err = i.run("tn_inst_prepare server"+show, "TN_CGROUP_FSTYPE=tmpfs", "INSTALL_K3S_CHANNEL=stable", "TN_K3S_UPGRADE=true", "INSTALL_K3S_EXEC=server") + out, err = i.run("tn_inst_prepare server"+show, "TN_CGROUP_FSTYPE=tmpfs", "INSTALL_K3S_CHANNEL=stable", "TN_K3S_UPGRADE=true", "TN_K3S_FORCE=true", "INSTALL_K3S_EXEC=server") if err != nil || !strings.Contains(out, "EXEC=[server]\n") { t.Errorf("%v\n%s", err, out) } diff --git a/pkg/templates/scripts/tn-k3s-install.sh b/pkg/templates/scripts/tn-k3s-install.sh index a4ec5e3..d712520 100644 --- a/pkg/templates/scripts/tn-k3s-install.sh +++ b/pkg/templates/scripts/tn-k3s-install.sh @@ -26,8 +26,11 @@ # downgrade: the datastore has been migrated), and so is a jump of more # than one minor version (v1.34 -> v1.36) unless TN_K3S_FORCE=true # (tnctl bootstrap --force): Kubernetes supports one minor at a time. -# A channel (stable, latest) cannot be compared before the installer -# resolves it: that is only a warning, pin an exact release to get the checks. +# A channel is checked too: a minor (v1.35) by its major.minor against the +# installed one (lower or other major: refused; more than +1: needs force); +# stable/latest/testing is resolved at update.k3s.io like the installer does +# and then checked as an exact release. A channel that cannot be resolved +# is refused unless TN_K3S_FORCE=true (then it goes on unchecked). # # An AGENT follows its server. A kubelet must never be newer than the apiserver it joins, so a new # agent (and an agent that TN_K3S_UPGRADE=true replaces) does not install the pin of the script: it @@ -336,6 +339,9 @@ tn_inst_normalize_pin() { v[0-9]*) printf '%s\n' "$INSTALL_K3S_VERSION" | grep -Eq '^v[0-9]+\.[0-9]+$' || return 0 ;; *) return 0 ;; esac + if [ -n "${INSTALL_K3S_CHANNEL:-}" ] && [ "$INSTALL_K3S_CHANNEL" != "$INSTALL_K3S_VERSION" ]; then + tn_inst_fatal "conflicting k3s requests: INSTALL_K3S_VERSION=$INSTALL_K3S_VERSION is a channel and INSTALL_K3S_CHANNEL=$INSTALL_K3S_CHANNEL is another. Set only one of them. $(tn_inst_state_note)" + fi INSTALL_K3S_CHANNEL=$INSTALL_K3S_VERSION export INSTALL_K3S_CHANNEL unset INSTALL_K3S_VERSION @@ -391,10 +397,44 @@ tn_inst_plan() { fi # TN_K3S_UPGRADE=true + if [ -z "$_tn_t" ]; then + # a channel: the installer picks the release, so find out which, or compare the minor + _tn_ch=${INSTALL_K3S_CHANNEL:-stable} + if printf '%s\n' "$_tn_ch" | grep -Eq '^v[0-9]+\.[0-9]+$'; then + tn_inst_parse "${_tn_ch}.0"; _tn_cmaj=$TN_INST_MAJ; _tn_cmin=$TN_INST_MIN + tn_inst_parse "$TN_INST_INSTALLED"; _tn_imaj=$TN_INST_MAJ; _tn_imin=$TN_INST_MIN + if [ "$_tn_cmaj" != "$_tn_imaj" ] || [ "$_tn_cmin" -lt "$_tn_imin" ]; then + tn_inst_fatal "refusing to downgrade k3s from $TN_INST_INSTALLED to the channel $_tn_ch (v$_tn_cmaj.$_tn_cmin is not v$_tn_imaj.$_tn_imin or a later minor of the same major): k3s cannot go back (the datastore has been migrated by the newer release). $(tn_inst_state_note). Use a channel of v$_tn_imaj.$_tn_imin or $_tn_imaj.$((_tn_imin + 1)), or restore the device from a backup" + fi + if [ $((_tn_cmin - _tn_imin)) -gt 1 ]; then + if [ "${TN_K3S_FORCE:-}" = true ]; then + tn_inst_warn "upgrading k3s from $TN_INST_INSTALLED to the channel $_tn_ch skips minor versions (TN_K3S_FORCE=true). Kubernetes supports one minor version at a time: this can leave the cluster in a state that does not start. Have a backup of the datastore" + else + tn_inst_fatal "refusing to upgrade k3s from $TN_INST_INSTALLED to the channel $_tn_ch: that is more than one minor version (v$_tn_imaj.$_tn_imin -> v$_tn_cmaj.$_tn_cmin). Upgrade one minor at a time (channel v$_tn_imaj.$((_tn_imin + 1)) first), or set TN_K3S_FORCE=true (tnctl bootstrap --force) if you know what you are doing. $(tn_inst_state_note)" + fi + fi + TN_INST_ACTION=upgrade + tn_inst_info "upgrading k3s from $TN_INST_INSTALLED to the latest release of the channel $_tn_ch (TN_K3S_UPGRADE=true). A server must be upgraded before its agents" + return 0 + fi + # stable, latest, testing: resolve like the installer does (the last path segment of the redirect) + TN_INST_RESOLVED=$(curl -w '%{url_effective}' -L -s -S -o /dev/null "https://update.k3s.io/v1-release/channels/$_tn_ch" 2>/dev/null | sed 's|^.*/||; s|%2[Bb]|+|') || TN_INST_RESOLVED= + if tn_inst_parse "$TN_INST_RESOLVED" 2>/dev/null; then + _tn_t=$TN_INST_RESOLVED + tn_inst_info "the k3s channel $_tn_ch resolves to $_tn_t: it is pinned, so that the installer gets what was checked" + INSTALL_K3S_VERSION=$_tn_t + export INSTALL_K3S_VERSION + unset INSTALL_K3S_CHANNEL + elif [ "${TN_K3S_FORCE:-}" = true ]; then + TN_INST_ACTION=upgrade + tn_inst_warn "TN_K3S_UPGRADE=true with the channel $_tn_ch, which cannot be resolved from here (update.k3s.io): the k3s installer picks the release, so a downgrade or a jump over several minor versions cannot be checked. TN_K3S_FORCE=true: going on without those checks. Pin an exact release (INSTALL_K3S_VERSION=v1.xx.y+k3sN) to get them" + return 0 + else + tn_inst_fatal "refusing to upgrade k3s from $TN_INST_INSTALLED to the channel $_tn_ch: the channel cannot be resolved to a release from here (update.k3s.io is not reachable), so a downgrade or a jump over several minor versions cannot be checked. Pin an exact release (INSTALL_K3S_VERSION=v1.xx.y+k3sN, tnctl bootstrap --k3s-version), or set TN_K3S_FORCE=true to go on without the checks. $(tn_inst_state_note)" + fi + fi if ! tn_inst_cmp "$_tn_t" "$TN_INST_INSTALLED"; then - TN_INST_ACTION=upgrade - tn_inst_warn "TN_K3S_UPGRADE=true with $_tn_want, which is not an exact release: the k3s installer picks the release, so a downgrade or a jump over several minor versions cannot be checked. Pin an exact release (INSTALL_K3S_VERSION=v1.xx.y+k3sN) to get those checks" - return 0 + tn_inst_fatal "cannot compare the requested k3s release '$_tn_t' with the installed $TN_INST_INSTALLED. Pin an exact release (INSTALL_K3S_VERSION=v1.xx.y+k3sN). $(tn_inst_state_note)" fi TN_INST_EFFECTIVE=$_tn_t case $TN_INST_CMP in