fix(agent): three backend fixes for FreeBSD, Docker rootless, and duplicate PVE hosts

FreeBSD auto-update (#1254): determineArch() now includes freebsd in its
OS switch, producing freebsd-amd64/arm64 instead of falling through to
a uname -m fallback that incorrectly returned linux-<arch>. FreeBSD agents
were downloading Linux ELF binaries and failing to exec them.

Docker rootless socket (#1200): buildRuntimeCandidates() now probes
/run/user/<uid>/docker.sock before the system-wide /var/run/docker.sock,
enabling auto-detection of Docker rootless installations.

Duplicate PVE/PBS hosts (#1245, #1252): handleSecureAutoRegister() now
deduplicates by host URL, updating the existing instance's token in-place
instead of appending a duplicate entry on each re-run of the setup script.

Fixes #1254
Fixes #1200
Fixes #1245
Fixes #1252

(cherry picked from commit 0f1d9e9b9fea6c8b9e65872e8a78e25f93653eef)
This commit is contained in:
rcourtman 2026-02-18 09:39:33 +00:00
parent 97aee77ae7
commit 7522f6599c
5 changed files with 92 additions and 3 deletions

View file

@ -85,6 +85,15 @@ func TestDetermineArchOverrides(t *testing.T) {
unameCommand = origUname
})
// FreeBSD is a first-class OS target for agent auto-updates.
// It should not fall back to uname mappings that force a linux-* arch.
runtimeGOOS = "freebsd"
runtimeGOARCH = "amd64"
unameCommand = func() ([]byte, error) { return nil, errors.New("uname should not be called for freebsd") }
if got := determineArch(); got != "freebsd-amd64" {
t.Fatalf("expected freebsd-amd64, got %q", got)
}
runtimeGOOS = "linux"
runtimeGOARCH = "arm"
if got := determineArch(); got != "linux-armv7" {

View file

@ -523,7 +523,7 @@ func determineArch() string {
// For known OS/arch combinations, return directly
switch os {
case "linux", "darwin", "windows":
case "linux", "darwin", "windows", "freebsd":
return fmt.Sprintf("%s-%s", os, arch)
}

View file

@ -5753,7 +5753,27 @@ func (h *ConfigHandlers) handleSecureAutoRegister(w http.ResponseWriter, r *http
MonitorStorage: true,
MonitorBackups: true,
}
h.getConfig(r.Context()).PVEInstances = append(h.getConfig(r.Context()).PVEInstances, pveNode)
// Deduplicate by host to keep secure auto-registration idempotent on reruns.
existingIndex := -1
for i, node := range h.getConfig(r.Context()).PVEInstances {
if node.Host == host {
existingIndex = i
break
}
}
if existingIndex >= 0 {
instance := &h.getConfig(r.Context()).PVEInstances[existingIndex]
instance.Host = host
instance.User = ""
instance.Password = ""
instance.TokenName = pveNode.TokenName
instance.TokenValue = pveNode.TokenValue
instance.Fingerprint = pveNode.Fingerprint
instance.VerifySSL = pveNode.VerifySSL
log.Info().Str("host", host).Str("type", "pve").Msg("Secure auto-register matched existing node by host; updated token in-place")
} else {
h.getConfig(r.Context()).PVEInstances = append(h.getConfig(r.Context()).PVEInstances, pveNode)
}
} else if req.Type == "pbs" {
pbsNode := config.PBSInstance{
Name: serverName,
@ -5768,7 +5788,27 @@ func (h *ConfigHandlers) handleSecureAutoRegister(w http.ResponseWriter, r *http
MonitorVerifyJobs: true,
MonitorPruneJobs: true,
}
h.getConfig(r.Context()).PBSInstances = append(h.getConfig(r.Context()).PBSInstances, pbsNode)
// Deduplicate by host to keep secure auto-registration idempotent on reruns.
existingIndex := -1
for i, node := range h.getConfig(r.Context()).PBSInstances {
if node.Host == host {
existingIndex = i
break
}
}
if existingIndex >= 0 {
instance := &h.getConfig(r.Context()).PBSInstances[existingIndex]
instance.Host = host
instance.User = ""
instance.Password = ""
instance.TokenName = pbsNode.TokenName
instance.TokenValue = pbsNode.TokenValue
instance.Fingerprint = pbsNode.Fingerprint
instance.VerifySSL = pbsNode.VerifySSL
log.Info().Str("host", host).Str("type", "pbs").Msg("Secure auto-register matched existing node by host; updated token in-place")
} else {
h.getConfig(r.Context()).PBSInstances = append(h.getConfig(r.Context()).PBSInstances, pbsNode)
}
}
// Save configuration

View file

@ -501,6 +501,13 @@ func buildRuntimeCandidates(preference RuntimeKind) []runtimeCandidate {
}
if preference == RuntimeDocker || preference == RuntimeAuto {
// Prefer rootless docker if present. Rootless installs use the per-user XDG runtime socket.
rootlessDocker := fmt.Sprintf("unix:///run/user/%d/docker.sock", os.Getuid())
add(runtimeCandidate{
host: rootlessDocker,
label: "docker rootless socket",
})
add(runtimeCandidate{
host: "unix:///var/run/docker.sock",
label: "default docker socket",

View file

@ -1001,8 +1001,12 @@ func TestBuildRuntimeCandidatesContent(t *testing.T) {
t.Run("auto includes both docker and podman", func(t *testing.T) {
candidates := buildRuntimeCandidates(RuntimeAuto)
hasDocker := false
hasRootlessDocker := false
hasPodman := false
for _, c := range candidates {
if c.label == "docker rootless socket" {
hasRootlessDocker = true
}
if c.label == "default docker socket" {
hasDocker = true
}
@ -1010,6 +1014,9 @@ func TestBuildRuntimeCandidatesContent(t *testing.T) {
hasPodman = true
}
}
if !hasRootlessDocker {
t.Error("auto preference should include docker rootless socket")
}
if !hasDocker {
t.Error("auto preference should include docker socket")
}
@ -1019,6 +1026,32 @@ func TestBuildRuntimeCandidatesContent(t *testing.T) {
})
}
func TestBuildRuntimeCandidatesDockerRootlessOrder(t *testing.T) {
t.Run("docker tries rootless before system socket", func(t *testing.T) {
candidates := buildRuntimeCandidates(RuntimeDocker)
rootlessIdx := -1
systemIdx := -1
for i, c := range candidates {
if c.label == "docker rootless socket" && rootlessIdx < 0 {
rootlessIdx = i
}
if c.label == "default docker socket" && systemIdx < 0 {
systemIdx = i
}
}
if rootlessIdx < 0 {
t.Fatal("expected docker rootless socket candidate")
}
if systemIdx < 0 {
t.Fatal("expected default docker socket candidate")
}
if rootlessIdx > systemIdx {
t.Fatalf("expected docker rootless socket to be tried before default docker socket; rootlessIdx=%d systemIdx=%d", rootlessIdx, systemIdx)
}
})
}
func TestBuildRuntimeCandidatesEnv(t *testing.T) {
t.Setenv("DOCKER_HOST", "unix:///tmp/docker.sock")
t.Setenv("CONTAINER_HOST", "unix:///tmp/container.sock")