Fix fresh Proxmox LXC installs defaulting to RC

This commit is contained in:
rcourtman 2026-04-20 23:11:46 +01:00
parent 4bd023fe1c
commit 4711d11163
2 changed files with 140 additions and 21 deletions

View file

@ -146,6 +146,24 @@ get_latest_release_from_redirect() {
return 0
}
read_configured_update_channel() {
if [[ "${IGNORE_CONFIGURED_UPDATE_CHANNEL:-false}" == "true" ]]; then
return 1
fi
if [[ ! -f "$CONFIG_DIR/system.json" ]]; then
return 1
fi
local configured_channel=""
configured_channel=$(grep -o '"updateChannel"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_DIR/system.json" 2>/dev/null | sed 's/.*"\([^"]*\)"$/\1/' || true)
if [[ -z "$configured_channel" ]]; then
return 1
fi
printf '%s\n' "$configured_channel"
}
resolve_install_script_download_url() {
if [[ -n "${FORCE_VERSION:-}" ]]; then
printf 'https://github.com/%s/releases/download/%s/install.sh\n' "$GITHUB_REPO" "$FORCE_VERSION"
@ -196,13 +214,11 @@ selected_update_channel() {
return 0
fi
if [[ -f "$CONFIG_DIR/system.json" ]]; then
local configured_channel=""
configured_channel=$(grep -o '"updateChannel"[[:space:]]*:[[:space:]]*"[^"]*"' "$CONFIG_DIR/system.json" 2>/dev/null | sed 's/.*"\([^"]*\)"$/\1/' || true)
if [[ -n "$configured_channel" ]]; then
printf '%s\n' "$configured_channel"
return 0
fi
local configured_channel=""
configured_channel=$(read_configured_update_channel 2>/dev/null || true)
if [[ -n "$configured_channel" ]]; then
printf '%s\n' "$configured_channel"
return 0
fi
printf 'stable\n'
@ -894,6 +910,12 @@ create_lxc_container() {
ADVANCED_MODE=false
;;
esac
# A Proxmox-host LXC install creates a brand-new Pulse instance inside the
# container. Host-local Pulse config must not silently switch that fresh
# install onto the prerelease channel unless the operator explicitly asked
# for it via --rc or --version.
IGNORE_CONFIGURED_UPDATE_CHANNEL=true
# Get next available container ID from Proxmox
local CTID=$(pvesh get /cluster/nextid 2>/dev/null || echo "100")
@ -2516,8 +2538,8 @@ resolve_target_release() {
if [[ -n "${FORCE_CHANNEL}" ]]; then
UPDATE_CHANNEL="${FORCE_CHANNEL}"
print_info "Using $UPDATE_CHANNEL channel from command line"
elif [[ -f "$CONFIG_DIR/system.json" ]]; then
CONFIGURED_CHANNEL=$(cat "$CONFIG_DIR/system.json" 2>/dev/null | grep -o '"updateChannel"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/' || true)
else
CONFIGURED_CHANNEL=$(read_configured_update_channel 2>/dev/null || true)
if [[ "$CONFIGURED_CHANNEL" == "rc" ]]; then
UPDATE_CHANNEL="rc"
print_info "Prerelease channel detected in configuration"
@ -3934,8 +3956,8 @@ main() {
# Allow override via command line
if [[ -n "${FORCE_CHANNEL}" ]]; then
UPDATE_CHANNEL="${FORCE_CHANNEL}"
elif [[ -f "$CONFIG_DIR/system.json" ]]; then
CONFIGURED_CHANNEL=$(cat "$CONFIG_DIR/system.json" 2>/dev/null | grep -o '"updateChannel"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"\([^"]*\)"$/\1/' || true)
else
CONFIGURED_CHANNEL=$(read_configured_update_channel 2>/dev/null || true)
if [[ "$CONFIGURED_CHANNEL" == "rc" ]]; then
UPDATE_CHANNEL="rc"
fi

View file

@ -806,10 +806,17 @@ func extractRootInstallShellFunction(t *testing.T, name string) string {
return string(match)
}
func extractSelectedUpdateChannelShellFunctions(t *testing.T) string {
t.Helper()
return extractRootInstallShellFunction(t, "read_configured_update_channel") + "\n" +
extractRootInstallShellFunction(t, "selected_update_channel")
}
func extractSetupAutoUpdatesShellFunctions(t *testing.T) string {
t.Helper()
return extractRootInstallShellFunction(t, "selected_update_channel") + "\n" +
return extractSelectedUpdateChannelShellFunctions(t) + "\n" +
extractRootInstallShellFunction(t, "repo_web_url") + "\n" +
extractRootInstallShellFunction(t, "configure_auto_update_script_repo") + "\n" +
extractRootInstallShellFunction(t, "setup_auto_updates")
@ -1258,7 +1265,7 @@ func TestBuildContainerInstallCommandPreservesForcedVersion(t *testing.T) {
SOURCE_BRANCH="main"
frontend_port="7655"
CONFIG_DIR="` + t.TempDir() + `"
` + extractRootInstallShellFunction(t, "selected_update_channel") + `
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
build_container_install_command
`
@ -1285,7 +1292,7 @@ func TestBuildContainerInstallCommandPreservesExplicitAutoUpdateDisable(t *testi
SOURCE_BRANCH="main"
frontend_port="7655"
CONFIG_DIR="` + t.TempDir() + `"
` + extractRootInstallShellFunction(t, "selected_update_channel") + `
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
build_container_install_command
`
@ -1313,7 +1320,7 @@ func TestBuildContainerInstallCommandPassesArchiveToContainer(t *testing.T) {
frontend_port="7655"
container_archive_dest="/tmp/pulse-v6.0.0-rc.1-linux-amd64.tar.gz"
CONFIG_DIR="` + t.TempDir() + `"
` + extractRootInstallShellFunction(t, "selected_update_channel") + `
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
build_container_install_command
`
@ -1341,7 +1348,7 @@ func TestBuildContainerInstallCommandQuotesArchivePath(t *testing.T) {
frontend_port="7655"
container_archive_dest="/tmp/pulse archive-linux-amd64.tar.gz"
CONFIG_DIR="` + t.TempDir() + `"
` + extractRootInstallShellFunction(t, "selected_update_channel") + `
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
build_container_install_command
`
@ -1368,7 +1375,7 @@ func TestBuildContainerInstallCommandPreservesRCChannel(t *testing.T) {
SOURCE_BRANCH="main"
frontend_port="7766"
CONFIG_DIR="` + t.TempDir() + `"
` + extractRootInstallShellFunction(t, "selected_update_channel") + `
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
build_container_install_command
`
@ -1385,6 +1392,39 @@ func TestBuildContainerInstallCommandPreservesRCChannel(t *testing.T) {
}
}
func TestBuildContainerInstallCommandIgnoresHostConfiguredRCChannelForFreshLXCInstall(t *testing.T) {
configDir := t.TempDir()
if err := os.WriteFile(filepath.Join(configDir, "system.json"), []byte(`{"updateChannel":"rc"}`), 0644); err != nil {
t.Fatalf("write system.json: %v", err)
}
script := `
FORCE_VERSION=""
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
IGNORE_CONFIGURED_UPDATE_CHANNEL="true"
auto_updates_flag=""
BUILD_FROM_SOURCE="false"
SOURCE_BRANCH="main"
frontend_port="7655"
CONFIG_DIR="` + configDir + `"
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
build_container_install_command
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
got := strings.TrimSpace(string(out))
want := "bash /tmp/install.sh --in-container"
if got != want {
t.Fatalf("install cmd = %q, want %q", got, want)
}
}
func TestPrintContainerRecoveryCommandPreservesForcedVersion(t *testing.T) {
script := `
FORCE_VERSION="v1.2.3"
@ -1396,7 +1436,7 @@ func TestPrintContainerRecoveryCommandPreservesForcedVersion(t *testing.T) {
frontend_port="7655"
CONFIG_DIR="` + t.TempDir() + `"
print_info() { printf '%s\n' "$1"; }
` + extractRootInstallShellFunction(t, "selected_update_channel") + `
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
` + extractRootInstallShellFunction(t, "print_container_recovery_command") + `
print_container_recovery_command
@ -1425,7 +1465,7 @@ func TestPrintContainerRecoveryCommandPreservesExplicitAutoUpdateDisable(t *test
frontend_port="7655"
CONFIG_DIR="` + t.TempDir() + `"
print_info() { printf '%s\n' "$1"; }
` + extractRootInstallShellFunction(t, "selected_update_channel") + `
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
` + extractRootInstallShellFunction(t, "print_container_recovery_command") + `
print_container_recovery_command
@ -1454,7 +1494,7 @@ func TestPrintContainerRecoveryCommandPreservesRCChannel(t *testing.T) {
frontend_port="7766"
CONFIG_DIR="` + t.TempDir() + `"
print_info() { printf '%s\n' "$1"; }
` + extractRootInstallShellFunction(t, "selected_update_channel") + `
` + extractSelectedUpdateChannelShellFunctions(t) + `
` + extractRootInstallShellFunction(t, "build_container_install_command") + `
` + extractRootInstallShellFunction(t, "print_container_recovery_command") + `
print_container_recovery_command
@ -1804,7 +1844,7 @@ func TestSelectedUpdateChannelTreatsPrereleaseVersionAsRC(t *testing.T) {
FORCE_VERSION="v1.2.3-rc.4"
UPDATE_CHANNEL=""
CONFIG_DIR="` + t.TempDir() + `"
` + extractRootInstallShellFunction(t, "selected_update_channel") + `
` + extractSelectedUpdateChannelShellFunctions(t) + `
selected_update_channel
`
@ -1817,6 +1857,63 @@ func TestSelectedUpdateChannelTreatsPrereleaseVersionAsRC(t *testing.T) {
}
}
func TestResolveTargetReleaseIgnoresHostConfiguredRCChannelForFreshLXCInstall(t *testing.T) {
tmpDir := t.TempDir()
configDir := filepath.Join(tmpDir, "config")
if err := os.MkdirAll(configDir, 0755); err != nil {
t.Fatalf("mkdir config dir: %v", err)
}
if err := os.WriteFile(filepath.Join(configDir, "system.json"), []byte(`{"updateChannel":"rc"}`), 0644); err != nil {
t.Fatalf("write system.json: %v", err)
}
timeoutPath := filepath.Join(tmpDir, "timeout")
curlPath := filepath.Join(tmpDir, "curl")
if err := os.WriteFile(timeoutPath, []byte("#!/usr/bin/env bash\nshift\nexec \"$@\"\n"), 0755); err != nil {
t.Fatalf("write timeout stub: %v", err)
}
curlStub := `#!/usr/bin/env bash
for arg in "$@"; do
if [[ "$arg" == "https://api.github.com/repos/rcourtman/Pulse/releases" ]]; then
printf '%s\n' '[{"draft":false,"prerelease":true,"tag_name":"v6.0.0-rc.1"},{"draft":false,"prerelease":false,"tag_name":"v5.1.28"}]'
exit 0
fi
done
echo "unexpected curl invocation: $*" >&2
exit 1
`
if err := os.WriteFile(curlPath, []byte(curlStub), 0755); err != nil {
t.Fatalf("write curl stub: %v", err)
}
script := `
PATH="` + tmpDir + `:$PATH"
GITHUB_REPO="rcourtman/Pulse"
FORCE_VERSION=""
FORCE_CHANNEL=""
UPDATE_CHANNEL=""
IGNORE_CONFIGURED_UPDATE_CHANNEL="true"
CONFIG_DIR="` + configDir + `"
print_info() { :; }
print_warn() { :; }
get_latest_release_from_redirect() { return 1; }
` + extractRootInstallShellFunction(t, "read_configured_update_channel") + `
` + extractRootInstallShellFunction(t, "resolve_target_release") + `
resolve_target_release
printf '%s\n' "$LATEST_RELEASE"
`
out, err := exec.Command("bash", "-c", script).CombinedOutput()
if err != nil {
t.Fatalf("bash: %v\n%s", err, out)
}
if got := strings.TrimSpace(string(out)); got != "v5.1.28" {
t.Fatalf("LATEST_RELEASE = %q, want v5.1.28", got)
}
}
func TestSetupAutoUpdatesCreatesSystemJSONWithSelectedChannel(t *testing.T) {
tmpDir := t.TempDir()
configDir := filepath.Join(tmpDir, "config")