fix: multiple agent installation and update issues

- Default enableDocker to false in UI to prevent unintended Docker
  agent activation on host-only installs (Related to #766)
- Deploy agent scripts and binaries during web UI upgrades, not just
  the main binary (Related to #760)
- Apply symlink resolution fix to standalone docker agent self-update
  to prevent cross-device rename failures (Related to #737)
This commit is contained in:
rcourtman 2025-11-27 15:49:03 +00:00
parent cb8c46cfb6
commit 6ff5ae4200
3 changed files with 118 additions and 9 deletions

View file

@ -107,7 +107,7 @@ export const UnifiedAgents: Component = () => {
const [lookupResult, setLookupResult] = createSignal<HostLookupResponse | null>(null);
const [lookupError, setLookupError] = createSignal<string | null>(null);
const [lookupLoading, setLookupLoading] = createSignal(false);
const [enableDocker, setEnableDocker] = createSignal(true); // Default to true for unified experience
const [enableDocker, setEnableDocker] = createSignal(false); // Default to false - user must opt-in for Docker monitoring
createEffect(() => {
if (requiresToken()) {

View file

@ -16,6 +16,7 @@ import (
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
@ -1640,6 +1641,12 @@ func isUnraid() bool {
return err == nil
}
// resolveSymlink resolves symlinks to get the real path of a file.
// This is needed for self-update because os.Rename() fails across filesystems.
func resolveSymlink(path string) (string, error) {
return filepath.EvalSymlinks(path)
}
// verifyELFMagic checks that the file is a valid ELF binary
func verifyELFMagic(path string) error {
f, err := os.Open(path)
@ -1701,6 +1708,19 @@ func (a *Agent) selfUpdate(ctx context.Context) error {
return fmt.Errorf("failed to get executable path: %w", err)
}
// Resolve symlinks to get the real path for atomic rename
// os.Rename() fails across filesystems, so we need the actual target path
realExecPath, err := resolveSymlink(execPath)
if err != nil {
a.logger.Debug().Err(err).Str("path", execPath).Msg("Failed to resolve symlinks, using original path")
realExecPath = execPath
} else if realExecPath != execPath {
a.logger.Debug().
Str("symlink", execPath).
Str("target", realExecPath).
Msg("Resolved symlink for self-update")
}
downloadBase := strings.TrimRight(target.URL, "/") + "/download/pulse-docker-agent"
archParam := determineSelfUpdateArch()
@ -1767,10 +1787,12 @@ func (a *Agent) selfUpdate(ctx context.Context) error {
checksumHeader := strings.TrimSpace(resp.Header.Get("X-Checksum-Sha256"))
// Create temporary file
tmpFile, err := os.CreateTemp("", "pulse-docker-agent-*.tmp")
// Create temporary file in the same directory as the target binary
// to ensure atomic rename works (os.Rename fails across filesystems)
targetDir := filepath.Dir(realExecPath)
tmpFile, err := os.CreateTemp(targetDir, "pulse-docker-agent-*.tmp")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
return fmt.Errorf("failed to create temp file in %s: %w", targetDir, err)
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath) // Clean up if something goes wrong
@ -1815,16 +1837,16 @@ func (a *Agent) selfUpdate(ctx context.Context) error {
return fmt.Errorf("failed to make temp file executable: %w", err)
}
// Create backup of current binary
backupPath := execPath + ".backup"
if err := os.Rename(execPath, backupPath); err != nil {
// Create backup of current binary (use realExecPath for atomic operations)
backupPath := realExecPath + ".backup"
if err := os.Rename(realExecPath, backupPath); err != nil {
return fmt.Errorf("failed to backup current binary: %w", err)
}
// Move new binary to current location
if err := os.Rename(tmpPath, execPath); err != nil {
if err := os.Rename(tmpPath, realExecPath); err != nil {
// Restore backup on failure
os.Rename(backupPath, execPath)
os.Rename(backupPath, realExecPath)
return fmt.Errorf("failed to replace binary: %w", err)
}

View file

@ -1229,6 +1229,93 @@ func (m *Manager) applyUpdateFiles(extractDir string) error {
}
}
// Deploy agent installation scripts from tarball
scriptsDir := filepath.Join(extractDir, "scripts")
if _, err := os.Stat(scriptsDir); err == nil {
destScriptsDir := "/opt/pulse/scripts"
if err := os.MkdirAll(destScriptsDir, 0755); err != nil {
log.Warn().Err(err).Msg("Failed to create scripts directory")
} else {
// List of agent scripts to deploy
agentScripts := []string{
"install-docker-agent.sh",
"install-container-agent.sh",
"install-host-agent.ps1",
"uninstall-host-agent.sh",
"uninstall-host-agent.ps1",
"install-sensor-proxy.sh",
"install-docker.sh",
"install.sh",
"install.ps1",
}
deployed := 0
for _, script := range agentScripts {
srcPath := filepath.Join(scriptsDir, script)
if _, err := os.Stat(srcPath); err == nil {
destPath := filepath.Join(destScriptsDir, script)
cmd = exec.Command("cp", srcPath, destPath)
if err := cmd.Run(); err != nil {
log.Warn().Err(err).Str("script", script).Msg("Failed to copy agent script")
continue
}
if err := os.Chmod(destPath, 0755); err != nil {
log.Warn().Err(err).Str("script", script).Msg("Failed to set script permissions")
}
deployed++
}
}
if deployed > 0 {
log.Info().Int("count", deployed).Msg("Deployed agent installation scripts")
}
}
}
// Deploy agent binaries from tarball (for serving to remote hosts)
binDir := filepath.Join(extractDir, "bin")
if _, err := os.Stat(binDir); err == nil {
destBinDir := "/opt/pulse/bin"
if err := os.MkdirAll(destBinDir, 0755); err != nil {
log.Warn().Err(err).Msg("Failed to create bin directory")
} else {
// Copy agent binaries (pulse-agent-*, pulse-docker-agent-*, pulse-host-agent-*)
entries, err := os.ReadDir(binDir)
if err == nil {
agentBinariesDeployed := 0
for _, entry := range entries {
name := entry.Name()
// Skip the main pulse binary (already handled above) and directories
if entry.IsDir() || name == "pulse" {
continue
}
// Copy agent binaries
if strings.HasPrefix(name, "pulse-agent-") ||
strings.HasPrefix(name, "pulse-docker-agent") ||
strings.HasPrefix(name, "pulse-host-agent") ||
name == "pulse-sensor-proxy" {
srcPath := filepath.Join(binDir, name)
destPath := filepath.Join(destBinDir, name)
cmd = exec.Command("cp", "-a", srcPath, destPath)
if err := cmd.Run(); err != nil {
log.Warn().Err(err).Str("binary", name).Msg("Failed to copy agent binary")
continue
}
// Set executable permission (skip for symlinks)
if info, err := os.Lstat(destPath); err == nil && info.Mode()&os.ModeSymlink == 0 {
if err := os.Chmod(destPath, 0755); err != nil {
log.Warn().Err(err).Str("binary", name).Msg("Failed to set binary permissions")
}
}
agentBinariesDeployed++
}
}
if agentBinariesDeployed > 0 {
log.Info().Int("count", agentBinariesDeployed).Msg("Deployed agent binaries")
}
}
}
}
// Set ownership if /opt/pulse exists
if _, err := os.Stat("/opt/pulse"); err == nil {
cmd = exec.Command("chown", "-R", "pulse:pulse", "/opt/pulse")