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:
parent
cb8c46cfb6
commit
6ff5ae4200
3 changed files with 118 additions and 9 deletions
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in a new issue