diff --git a/internal/agentupdate/coverage_test.go b/internal/agentupdate/coverage_test.go index 79010ad51..2b9154c13 100644 --- a/internal/agentupdate/coverage_test.go +++ b/internal/agentupdate/coverage_test.go @@ -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" { diff --git a/internal/agentupdate/update.go b/internal/agentupdate/update.go index 0204f98dd..652c8c2f8 100644 --- a/internal/agentupdate/update.go +++ b/internal/agentupdate/update.go @@ -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) } diff --git a/internal/api/config_handlers.go b/internal/api/config_handlers.go index 14ed2a9d7..47e7549e6 100644 --- a/internal/api/config_handlers.go +++ b/internal/api/config_handlers.go @@ -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 diff --git a/internal/dockeragent/agent.go b/internal/dockeragent/agent.go index 8fa3435eb..2ce6a3946 100644 --- a/internal/dockeragent/agent.go +++ b/internal/dockeragent/agent.go @@ -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", diff --git a/internal/dockeragent/agent_internal_test.go b/internal/dockeragent/agent_internal_test.go index 783befc11..1a5875eba 100644 --- a/internal/dockeragent/agent_internal_test.go +++ b/internal/dockeragent/agent_internal_test.go @@ -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")