mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-14 00:00:30 +00:00
883e951023
/download/pulse-agent served whatever agent artifact sat on disk with no relation to the running build. Local agent binaries are build outputs that nothing refreshes on their own, so they go stale silently: a dev backend was found serving a four-week-old v6.0.5 agent while reporting 6.2.0-rc.8. Staleness is not cosmetic. The installer renders its service wrapper from the server's current template, so an agent predating a flag that template now passes exits immediately with "flag provided but not defined" and crash-loops under its watchdog. That is how a real host lost its agent: the version mismatch was reported only as an installer warning, after download, easy to read as noise. Validation already scanned the binary for its report-contract endpoints, so the version check joins that same single pass and rejects a binary that does not carry this server's agent version. Refusal is loud where the old warning was quiet: a dev server answers 404 naming the stale path and the build command, and a published release falls through to the existing release-asset proxy and fetches the matching version, which makes production self-healing rather than silently downgraded. The expected version resolves through updates.GetCurrentVersion rather than the compiled-in serverVersion. The first cut of this guard used serverVersion and was inert on exactly the builds that need it: the enterprise binary compiles in "dev-pro", no version parser accepts it, and the check disabled itself. It passed its unit tests and still served the stale binary; only replaying the real v6.0.5 artifact through the running server exposed it. "dev-pro" is now pinned in the version table with that reasoning attached. The shared download-test fixture built a binary carrying the report endpoint but no version string, which a real agent always has, so it now stamps the expected version. Verified non-vacuous in both directions: those tests fail with the guard active and the thin fixture, and pass with a faithful one. Contract-Neutral: storage-recovery is pulled in only by the broad internal/api/ Extension Points prefix and this change does not move that boundary: it constrains which agent binary /download/pulse-agent serves, touching no storage provider, backup target, recovery repository or protected-workload evidence. The agent-lifecycle, deployment-installability and api-contracts deltas staged here cover every boundary the change actually moves.
649 lines
25 KiB
Go
649 lines
25 KiB
Go
package api
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/updates"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func setupUnifiedAgentRouter(t *testing.T) (*Router, string) {
|
|
tempDir := t.TempDir()
|
|
|
|
// Create required directories
|
|
err := os.MkdirAll(filepath.Join(tempDir, "scripts"), 0755)
|
|
require.NoError(t, err)
|
|
err = os.MkdirAll(filepath.Join(tempDir, "bin"), 0755)
|
|
require.NoError(t, err)
|
|
|
|
router := &Router{
|
|
projectRoot: tempDir,
|
|
checksumCache: make(map[string]checksumCacheEntry),
|
|
}
|
|
|
|
return router, tempDir
|
|
}
|
|
|
|
// expectedAgentVersionForTest mirrors what the download handler will look for,
|
|
// so fixtures that stand in for a healthy agent carry the version a real build
|
|
// would. Without it every "valid" fixture reads as a stale build.
|
|
func expectedAgentVersionForTest() string {
|
|
versionInfo, err := updates.GetCurrentVersion()
|
|
if err != nil || versionInfo == nil {
|
|
return ""
|
|
}
|
|
return expectedAgentBinaryVersion(versionInfo.Version)
|
|
}
|
|
|
|
func validTestUnifiedAgentBinary(suffix string) []byte {
|
|
return []byte("ELF test binary " + canonicalUnifiedAgentReportPath + " " +
|
|
expectedAgentVersionForTest() + " " + suffix)
|
|
}
|
|
|
|
func staleTestUnifiedAgentBinary(suffix string) []byte {
|
|
return []byte("ELF stale binary " + legacyUnifiedAgentReportPath + " " + suffix)
|
|
}
|
|
|
|
func encodedTestSSHSignature(payload string) string {
|
|
return encodeSSHSignatureForHeader([]byte(payload))
|
|
}
|
|
|
|
func TestDownloadInstallScript_Local(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
|
|
// Create dummy script
|
|
scriptContent := "#!/bin/bash\necho 'installing'"
|
|
scriptPath := filepath.Join(tempDir, "scripts", "install.sh")
|
|
err := os.WriteFile(scriptPath, []byte(scriptContent), 0644)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/install.sh", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedInstallScript(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, scriptContent, w.Body.String())
|
|
assert.Equal(t, "text/x-shellscript", w.Header().Get("Content-Type"))
|
|
}
|
|
|
|
// TestDownloadInstallScript_PublishedReleaseUnsignedLocalServesAgentInstaller
|
|
// pins the issue #1470 closure: when the local agent installer is present on a
|
|
// published-release server but its .sig/.sshsig sidecars are not, the endpoint
|
|
// must serve the LOCAL agent installer, never proxy the GitHub install.sh asset
|
|
// (which is the SERVER installer). This is the state every pre-fix LXC/systemd
|
|
// install is in until it redeploys the sidecars.
|
|
func TestDownloadInstallScript_PublishedReleaseUnsignedLocalServesAgentInstaller(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.6"
|
|
// A client that fails any outbound call, so a regression that re-introduces
|
|
// the GitHub proxy fallback surfaces as a test failure rather than silently
|
|
// serving the server installer.
|
|
router.installScriptClient = newTestInstallScriptClient(t, http.MethodGet, "", 0, "", errors.New("proxy must not be used"))
|
|
|
|
scriptContent := "#!/usr/bin/env bash\n# Pulse Unified Agent Installer\necho 'agent'"
|
|
scriptPath := filepath.Join(tempDir, "scripts", "install.sh")
|
|
require.NoError(t, os.WriteFile(scriptPath, []byte(scriptContent), 0644))
|
|
// Note: no .sig / .sshsig sidecars written.
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/install.sh", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedInstallScript(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, scriptContent, w.Body.String())
|
|
assert.NotEqual(t, "github-fallback", w.Header().Get("X-Served-From"))
|
|
}
|
|
|
|
func TestDownloadInstallScriptPS_Local(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
|
|
// Create dummy script
|
|
scriptContent := "Write-Host 'installing'"
|
|
scriptPath := filepath.Join(tempDir, "scripts", "install.ps1")
|
|
err := os.WriteFile(scriptPath, []byte(scriptContent), 0644)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/install.ps1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedInstallScriptPS(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, scriptContent, w.Body.String())
|
|
assert.Equal(t, "text/plain", w.Header().Get("Content-Type"))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_Local_Generic(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
|
|
// Create dummy binary in project root / bin
|
|
binContent := validTestUnifiedAgentBinary("generic")
|
|
binPath := filepath.Join(tempDir, "bin", "pulse-agent")
|
|
err := os.WriteFile(binPath, binContent, 0755)
|
|
require.NoError(t, err)
|
|
|
|
// Since cachedSHA256 might not be initialized or working without real file usage pattern,
|
|
// checking if our manual Router setup handles it.
|
|
// cachedSHA256 needs 'checksumCache' map initialized which we did in setupUnifiedAgentRouter.
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
// Handle calls r.cachedSHA256 which reads the file
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
// We expect success if cachedSHA256 works
|
|
if w.Code == http.StatusInternalServerError {
|
|
// If cachedSHA256 fails (maybe because it's not exported or implemented elsewhere
|
|
// and depends on something I missed), we will fail here.
|
|
// cachedSHA256 is called in unified_agent.go but defined presumably in router.go or router_utils.go (unexported).
|
|
// I initialized checksumCache so it should work.
|
|
t.Logf("Handler returned 500: %s", w.Body.String())
|
|
}
|
|
|
|
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
|
|
assert.Equal(t, string(binContent), w.Body.String())
|
|
|
|
// Verify Checksum Header
|
|
hash := sha256.Sum256(binContent)
|
|
expectedChecksum := hex.EncodeToString(hash[:])
|
|
assert.Equal(t, expectedChecksum, w.Header().Get("X-Checksum-Sha256"))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_Local_SpecificArch(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
|
|
// Create dummy binary for linux-amd64
|
|
binContent := validTestUnifiedAgentBinary("linux-amd64")
|
|
binPath := filepath.Join(tempDir, "bin", "pulse-agent-linux-amd64")
|
|
err := os.WriteFile(binPath, binContent, 0755)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, string(binContent), w.Body.String())
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_LocalReleaseBinaryIncludesSignatureHeader(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0"
|
|
|
|
binContent := validTestUnifiedAgentBinary("linux-amd64")
|
|
binPath := filepath.Join(tempDir, "bin", "pulse-agent-linux-amd64")
|
|
err := os.WriteFile(binPath, binContent, 0755)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(binPath+".sig", []byte("signed-local-agent"), 0644)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile(binPath+".sshsig", []byte("signed-local-agent-ssh"), 0644)
|
|
require.NoError(t, err)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "signed-local-agent", w.Header().Get(signatureHeaderName))
|
|
assert.Equal(t, encodedTestSSHSignature("signed-local-agent-ssh"), w.Header().Get(sshSignatureHeaderName))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_SkipsStaleLocalBinaryAndProxies(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.1"
|
|
|
|
stalePath := filepath.Join(tempDir, "bin", "pulse-agent-linux-amd64")
|
|
require.NoError(t, os.WriteFile(stalePath, staleTestUnifiedAgentBinary("linux-amd64"), 0755))
|
|
|
|
binaryContent := "fresh github binary"
|
|
expectedURL := "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64"
|
|
router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{
|
|
{Method: http.MethodGet, URL: expectedURL, Status: http.StatusOK, Body: binaryContent},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sig", Status: http.StatusOK, Body: "signed-agent"},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sshsig", Status: http.StatusOK, Body: "signed-agent-ssh"},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, binaryContent, w.Body.String())
|
|
assert.Equal(t, "github-proxy", w.Header().Get("X-Served-From"))
|
|
assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName))
|
|
assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_DevModeRejectsStaleLocalBinary(t *testing.T) {
|
|
router, tempDir := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "dev"
|
|
|
|
stalePath := filepath.Join(tempDir, "bin", "pulse-agent-linux-amd64")
|
|
require.NoError(t, os.WriteFile(stalePath, staleTestUnifiedAgentBinary("linux-amd64"), 0755))
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusNotFound, w.Code)
|
|
assert.Contains(t, w.Body.String(), "stale or incompatible")
|
|
assert.Contains(t, w.Body.String(), legacyUnifiedAgentReportPath)
|
|
assert.Contains(t, w.Body.String(), "go build -o bin/pulse-agent-linux-amd64")
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.1"
|
|
|
|
// Ensure NO local files exist (temp dir is empty of binaries)
|
|
// Set up a mock HTTP client to simulate GitHub response
|
|
binaryContent := "fake binary content for proxy test"
|
|
expectedURL := "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64"
|
|
router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{
|
|
{Method: http.MethodGet, URL: expectedURL, Status: http.StatusOK, Body: binaryContent},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sig", Status: http.StatusOK, Body: "signed-agent"},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sshsig", Status: http.StatusOK, Body: "signed-agent-ssh"},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
// Should proxy the binary with checksum header instead of redirecting
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, binaryContent, w.Body.String())
|
|
assert.Equal(t, "github-proxy", w.Header().Get("X-Served-From"))
|
|
|
|
// Verify checksum header is present and correct
|
|
hash := sha256.Sum256([]byte(binaryContent))
|
|
expectedChecksum := hex.EncodeToString(hash[:])
|
|
assert.Equal(t, expectedChecksum, w.Header().Get("X-Checksum-Sha256"))
|
|
assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName))
|
|
assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub_UsesConfiguredRepo(t *testing.T) {
|
|
t.Setenv("PULSE_GITHUB_REPO", "example/pulse-fork")
|
|
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.1"
|
|
|
|
binaryContent := "fake binary content for proxy test"
|
|
expectedURL := "https://github.com/example/pulse-fork/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64"
|
|
router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{
|
|
{Method: http.MethodGet, URL: expectedURL, Status: http.StatusOK, Body: binaryContent},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sig", Status: http.StatusOK, Body: "signed-agent"},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sshsig", Status: http.StatusOK, Body: "signed-agent-ssh"},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, binaryContent, w.Body.String())
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub_Windows(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.1"
|
|
|
|
binaryContent := "MZ fake windows binary"
|
|
expectedURL := "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-windows-amd64.exe"
|
|
router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{
|
|
{Method: http.MethodGet, URL: expectedURL, Status: http.StatusOK, Body: binaryContent},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sig", Status: http.StatusOK, Body: "signed-agent"},
|
|
{Method: http.MethodGet, URL: expectedURL + ".sshsig", Status: http.StatusOK, Body: "signed-agent-ssh"},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=windows-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, binaryContent, w.Body.String())
|
|
assert.NotEmpty(t, w.Header().Get("X-Checksum-Sha256"))
|
|
assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName))
|
|
assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub_ArchiveFallback_Darwin(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v9.9.9"
|
|
|
|
binaryContent := []byte("darwin arm64 binary payload")
|
|
archivePayload := buildTestTarGz(t, "pulse-agent-darwin-arm64", binaryContent)
|
|
binaryURL := "https://github.com/rcourtman/Pulse/releases/download/v9.9.9/pulse-agent-darwin-arm64"
|
|
signatureURL := binaryURL + ".sig"
|
|
sshSignatureURL := binaryURL + ".sshsig"
|
|
archiveURL := "https://github.com/rcourtman/Pulse/releases/download/v9.9.9/pulse-agent-v9.9.9-darwin-arm64.tar.gz"
|
|
|
|
router.installScriptClient = &http.Client{
|
|
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
switch req.URL.String() {
|
|
case binaryURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusNotFound,
|
|
Status: "404 Not Found",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("not found")),
|
|
}, nil
|
|
case archiveURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(bytes.NewReader(archivePayload)),
|
|
}, nil
|
|
case signatureURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("signed-agent")),
|
|
}, nil
|
|
case sshSignatureURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("signed-agent-ssh")),
|
|
}, nil
|
|
default:
|
|
t.Fatalf("unexpected URL: %s", req.URL.String())
|
|
return nil, nil
|
|
}
|
|
}),
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=darwin-arm64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "github-proxy-archive", w.Header().Get("X-Served-From"))
|
|
assert.Equal(t, string(binaryContent), w.Body.String())
|
|
|
|
hash := sha256.Sum256(binaryContent)
|
|
expectedChecksum := hex.EncodeToString(hash[:])
|
|
assert.Equal(t, expectedChecksum, w.Header().Get("X-Checksum-Sha256"))
|
|
assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName))
|
|
assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub_ArchiveFallback_UsesConfiguredRepo(t *testing.T) {
|
|
t.Setenv("PULSE_GITHUB_REPO", "example/pulse-fork")
|
|
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v9.9.9"
|
|
|
|
binaryContent := []byte("darwin arm64 binary payload")
|
|
archivePayload := buildTestTarGz(t, "pulse-agent-darwin-arm64", binaryContent)
|
|
binaryURL := "https://github.com/example/pulse-fork/releases/download/v9.9.9/pulse-agent-darwin-arm64"
|
|
signatureURL := binaryURL + ".sig"
|
|
sshSignatureURL := binaryURL + ".sshsig"
|
|
archiveURL := "https://github.com/example/pulse-fork/releases/download/v9.9.9/pulse-agent-v9.9.9-darwin-arm64.tar.gz"
|
|
|
|
router.installScriptClient = &http.Client{
|
|
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
switch req.URL.String() {
|
|
case binaryURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusNotFound,
|
|
Status: "404 Not Found",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("not found")),
|
|
}, nil
|
|
case archiveURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(bytes.NewReader(archivePayload)),
|
|
}, nil
|
|
case signatureURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("signed-agent")),
|
|
}, nil
|
|
case sshSignatureURL:
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Status: "200 OK",
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(strings.NewReader("signed-agent-ssh")),
|
|
}, nil
|
|
default:
|
|
t.Fatalf("unexpected URL: %s", req.URL.String())
|
|
return nil, nil
|
|
}
|
|
}),
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=darwin-arm64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "github-proxy-archive", w.Header().Get("X-Served-From"))
|
|
assert.Equal(t, string(binaryContent), w.Body.String())
|
|
assert.Equal(t, "signed-agent", w.Header().Get(signatureHeaderName))
|
|
assert.Equal(t, encodedTestSSHSignature("signed-agent-ssh"), w.Header().Get(sshSignatureHeaderName))
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub_NotFound(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.1"
|
|
|
|
router.installScriptClient = newTestInstallScriptClientSequence(t, []expectedHTTPExchange{
|
|
{Method: http.MethodGet, URL: "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64", Status: http.StatusNotFound, Body: ""},
|
|
{Method: http.MethodGet, URL: "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-v6.0.0-rc.1-linux-amd64.tar.gz", Status: http.StatusNotFound, Body: ""},
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_ProxyFromGitHub_Error(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-rc.1"
|
|
|
|
// GitHub is unreachable
|
|
router.installScriptClient = newTestInstallScriptClient(t, http.MethodGet, "https://github.com/rcourtman/Pulse/releases/download/v6.0.0-rc.1/pulse-agent-linux-amd64", 0, "", errors.New("connection refused"))
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusServiceUnavailable, w.Code)
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_DevPrereleaseRejectsGitHubFallback(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "v6.0.0-dev"
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=linux-amd64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusNotFound, w.Code)
|
|
assert.Contains(t, w.Body.String(), "dev mode")
|
|
assert.Contains(t, w.Body.String(), "go build -o bin/pulse-agent-linux-amd64")
|
|
}
|
|
|
|
func TestDownloadUnifiedAgent_DevModeReportsRequestedLocalBuildCommand(t *testing.T) {
|
|
router, _ := setupUnifiedAgentRouter(t)
|
|
router.serverVersion = "dev"
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/install/agent?arch=darwin-arm64", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.handleDownloadUnifiedAgent(w, req)
|
|
|
|
require.Equal(t, http.StatusNotFound, w.Code)
|
|
assert.Contains(t, w.Body.String(), "Agent binary not found for \"darwin-arm64\" in dev mode.")
|
|
assert.Contains(t, w.Body.String(), "CGO_ENABLED=0 GOOS=darwin GOARCH=arm64")
|
|
assert.Contains(t, w.Body.String(), "go build -o bin/pulse-agent-darwin-arm64 ./cmd/pulse-agent")
|
|
assert.NotContains(t, w.Body.String(), "bin/pulse-agent-linux-amd64")
|
|
}
|
|
|
|
func TestUnifiedAgentLocalBuildCommandHandlesArmVariants(t *testing.T) {
|
|
assert.Equal(
|
|
t,
|
|
"CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o bin/pulse-agent-linux-armv7 ./cmd/pulse-agent",
|
|
unifiedAgentLocalBuildCommand("linux-armv7"),
|
|
)
|
|
assert.Equal(
|
|
t,
|
|
"CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o bin/pulse-agent-linux-armv6 ./cmd/pulse-agent",
|
|
unifiedAgentLocalBuildCommand("linux-armv6"),
|
|
)
|
|
}
|
|
|
|
func TestNormalizeUnifiedAgentArch(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"amd64", "linux-amd64"},
|
|
{"x86_64", "linux-amd64"},
|
|
{"linux-amd64", "linux-amd64"},
|
|
{"arm64", "linux-arm64"},
|
|
{"aarch64", "linux-arm64"},
|
|
{"windows-amd64", "windows-amd64"},
|
|
{"darwin-arm64", "darwin-arm64"},
|
|
{"unknown", ""},
|
|
{"", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, normalizeUnifiedAgentArch(tt.input))
|
|
})
|
|
}
|
|
}
|
|
|
|
func buildTestTarGz(t *testing.T, name string, payload []byte) []byte {
|
|
t.Helper()
|
|
|
|
var buf bytes.Buffer
|
|
gzw := gzip.NewWriter(&buf)
|
|
tw := tar.NewWriter(gzw)
|
|
|
|
header := &tar.Header{
|
|
Name: name,
|
|
Mode: 0o755,
|
|
Size: int64(len(payload)),
|
|
}
|
|
require.NoError(t, tw.WriteHeader(header))
|
|
_, err := tw.Write(payload)
|
|
require.NoError(t, err)
|
|
require.NoError(t, tw.Close())
|
|
require.NoError(t, gzw.Close())
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// writeAgentBinaryFixture builds a stand-in for a compiled agent. The real
|
|
// validator is a byte scan, so a file carrying the same needles exercises it
|
|
// faithfully without shipping a multi-megabyte binary into the test tree.
|
|
func writeAgentBinaryFixture(t *testing.T, reportPath string, versionString string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "pulse-agent")
|
|
body := "\x7fELF padding " + reportPath + " more padding " + versionString + " trailing\n"
|
|
if err := os.WriteFile(path, []byte(body), 0o755); err != nil {
|
|
t.Fatalf("write agent fixture: %v", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestExpectedAgentBinaryVersionDropsBuildMetadata(t *testing.T) {
|
|
cases := map[string]string{
|
|
// A dev server carries git metadata the agent never has.
|
|
"6.2.0-rc.8+git.44.gdd72bd149.dirty": "v6.2.0-rc.8",
|
|
"6.2.0-rc.8": "v6.2.0-rc.8",
|
|
"v6.1.2": "v6.1.2",
|
|
// Unknown versions disable the check rather than rejecting everything.
|
|
// "dev-pro" is the enterprise build's compiled-in placeholder: it is
|
|
// why the caller resolves the version through updates.GetCurrentVersion
|
|
// instead of the compiled-in serverVersion, because a dev server is
|
|
// exactly where agent binaries go stale and this value would silently
|
|
// switch the freshness check off there.
|
|
"dev": "",
|
|
"dev-pro": "",
|
|
"": "",
|
|
"not-a-version": "",
|
|
}
|
|
for input, want := range cases {
|
|
if got := expectedAgentBinaryVersion(input); got != want {
|
|
t.Errorf("expectedAgentBinaryVersion(%q) = %q, want %q", input, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateUnifiedAgentBinaryRejectsStaleBuild(t *testing.T) {
|
|
// The shape that broke a live host: a binary on the canonical report
|
|
// contract, so every existing check passes, but built weeks earlier.
|
|
stale := writeAgentBinaryFixture(t, canonicalUnifiedAgentReportPath, "v6.0.5-54-gc862fb0ca0")
|
|
|
|
if err := validateUnifiedAgentBinary(stale, "v6.2.0-rc.8"); err == nil {
|
|
t.Fatal("a binary that does not carry this server's agent version must be refused")
|
|
} else if !strings.Contains(err.Error(), "stale build") {
|
|
t.Fatalf("error should name staleness, got %v", err)
|
|
}
|
|
|
|
// Premise check: without the version guard this same binary is accepted,
|
|
// which is exactly how it reached a host in the first place.
|
|
if err := validateUnifiedAgentBinary(stale, ""); err != nil {
|
|
t.Fatalf("premise check failed: the stale binary is supposed to pass every other check, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateUnifiedAgentBinaryAcceptsMatchingBuild(t *testing.T) {
|
|
current := writeAgentBinaryFixture(t, canonicalUnifiedAgentReportPath, "v6.2.0-rc.8")
|
|
if err := validateUnifiedAgentBinary(current, "v6.2.0-rc.8"); err != nil {
|
|
t.Fatalf("a binary carrying this server's agent version must be served: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateUnifiedAgentBinaryStillRejectsLegacyContract(t *testing.T) {
|
|
// The version guard must not displace the endpoint guard: a correctly
|
|
// versioned binary on the deprecated report path is still refused.
|
|
legacy := writeAgentBinaryFixture(t, legacyUnifiedAgentReportPath, "v6.2.0-rc.8")
|
|
if err := validateUnifiedAgentBinary(legacy, "v6.2.0-rc.8"); err == nil {
|
|
t.Fatal("a binary on the deprecated report endpoint must still be refused")
|
|
}
|
|
}
|