diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index b1b1a2e477..2d95d89cf5 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -169,6 +169,17 @@ server-side update execution surfaces. handle `--version)` in its argument parser, contains the agent installer banner string, or does not print the server installer's version-pinning help line when invoked with `--help`. + Because the served `/install.sh` endpoint hands out the AGENT installer, a + server install must also deploy the script's `.sig` / `.sshsig` sidecars next + to it (`/opt/pulse/scripts/install.sh.sig`, `.sshsig`). For published-release + versions `internal/api/unified_agent.go::handleDownloadInstallScriptCommon` + only serves the local copy when both sidecars are present; otherwise it + proxies the top-level GitHub `install.sh` asset, which (per the rule above) is + the SERVER installer, so the "Install on Linux" agent wizard's + `--url` / `--token-file` command is rejected with "Unknown option". The + Docker image deploys these sidecars (`Dockerfile`); `install.sh`'s + `deploy_agent_scripts` must deploy them for LXC / systemd installs too. This + parity gap shipped as the rc.6 agent-wizard regression (issue #1470). Deployment bootstrap token behavior remains a deployment-installability trust boundary even when the handler is API-owned. `internal/api/deploy_handlers.go` must preserve server-derived `owner_user_id` lineage on bootstrap tokens and diff --git a/install.sh b/install.sh index 6daeaebcdc..35384fecd5 100755 --- a/install.sh +++ b/install.sh @@ -3365,6 +3365,23 @@ deploy_agent_scripts() { cp "$extract_dir/scripts/$script" "$INSTALL_DIR/scripts/$script" chmod 755 "$INSTALL_DIR/scripts/$script" chown pulse:pulse "$INSTALL_DIR/scripts/$script" + # Deploy the detached release signatures next to the script. The + # running server serves these local copies at /install.sh and + # /install.ps1 (the "Install on Linux/Windows" agent wizard), and for + # published releases the handler only serves the local file when its + # .sig and .sshsig sidecars are present. Without them it falls back to + # proxying the top-level GitHub install.sh asset, which is the SERVER + # installer, not the agent installer, so the wizard's --url/--token-file + # command is rejected with "Unknown option". The Docker image deploys + # these same sidecars (see Dockerfile); LXC/systemd installs must match. + local sidecar + for sidecar in "${script}.sig" "${script}.sshsig"; do + if [[ -f "$extract_dir/scripts/$sidecar" ]]; then + cp "$extract_dir/scripts/$sidecar" "$INSTALL_DIR/scripts/$sidecar" + chmod 644 "$INSTALL_DIR/scripts/$sidecar" + chown pulse:pulse "$INSTALL_DIR/scripts/$sidecar" + fi + done deployed=$((deployed + 1)) fi done diff --git a/scripts/installtests/root_install_sh_test.go b/scripts/installtests/root_install_sh_test.go index edf84d1c24..2cf557a468 100644 --- a/scripts/installtests/root_install_sh_test.go +++ b/scripts/installtests/root_install_sh_test.go @@ -564,3 +564,52 @@ func TestOperatorInstallDocsAvoidUnverifiedBootstrapAndFloatingImageTags(t *test } } } + +// TestRootInstallDeployAgentScriptsDeploysSignatureSidecars guards the fix for +// the rc.6 "Install on Linux" agent-wizard regression (issue #1470). The +// running server serves /opt/pulse/scripts/install.sh at its /install.sh +// endpoint, but for published releases the handler only serves that local copy +// when its .sig and .sshsig sidecars are present next to it; otherwise it +// proxies the top-level GitHub install.sh asset, which is the SERVER installer +// (not the agent installer) and rejects the wizard's --url/--token-file flags. +// The Docker image deploys these sidecars; deploy_agent_scripts must too. +func TestRootInstallDeployAgentScriptsDeploysSignatureSidecars(t *testing.T) { + extractDir := t.TempDir() + installDir := t.TempDir() + + scriptsSrc := filepath.Join(extractDir, "scripts") + if err := os.MkdirAll(scriptsSrc, 0o755); err != nil { + t.Fatalf("mkdir scripts: %v", err) + } + for _, name := range []string{ + "install.sh", "install.sh.sig", "install.sh.sshsig", + "install.ps1", "install.ps1.sig", "install.ps1.sshsig", + } { + if err := os.WriteFile(filepath.Join(scriptsSrc, name), []byte("payload-"+name), 0o644); err != nil { + t.Fatalf("write %s: %v", name, err) + } + } + + script := ` + set -euo pipefail + print_warn() { :; } + print_success() { :; } + chown() { :; } + INSTALL_DIR="` + installDir + `" +` + extractRootInstallShellFunction(t, "deploy_agent_scripts") + ` + deploy_agent_scripts "` + extractDir + `" + ` + + if out, err := exec.Command("bash", "-c", script).CombinedOutput(); err != nil { + t.Fatalf("deploy_agent_scripts failed: %v\n%s", err, out) + } + + for _, name := range []string{ + "install.sh", "install.sh.sig", "install.sh.sshsig", + "install.ps1", "install.ps1.sig", "install.ps1.sshsig", + } { + if _, err := os.Stat(filepath.Join(installDir, "scripts", name)); err != nil { + t.Fatalf("deploy_agent_scripts did not deploy %s next to the served script: %v", name, err) + } + } +}