Align Windows agent state with the installer

Use the installer-owned ProgramData directory consistently for token lookup, enrollment state, agent identity, receipts, and service runtime data. Pass that directory explicitly to the Windows service and retain the existing Linux default elsewhere.

Contract-Neutral: Windows agent state-path alignment preserves the existing installer and agent public contract
This commit is contained in:
rcourtman
2026-07-19 19:41:57 +01:00
parent ed492cce4d
commit 3f405af0c6
4 changed files with 44 additions and 12 deletions
+28 -11
View File
@@ -14,6 +14,7 @@ import (
"os/signal"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
@@ -1027,7 +1028,7 @@ func loadConfig(args []string, getenv func(string) string) (Config, error) {
kubeIncludeAllPodsFlag := fs.Bool("kube-include-all-pods", utils.ParseBool(envKubeIncludeAllPods), "Include all non-succeeded pods (may be large)")
kubeIncludeAllDeploymentsFlag := fs.Bool("kube-include-all-deployments", utils.ParseBool(envKubeIncludeAllDeployments), "Include all deployments, not just problem ones")
kubeMaxPodsFlag := fs.Int("kube-max-pods", defaultInt(envKubeMaxPods, 200), "Max pods included in report")
stateDirFlag := fs.String("state-dir", envStateDir, "Persistent state directory (default: /var/lib/pulse-agent)")
stateDirFlag := fs.String("state-dir", envStateDir, "Persistent state directory (default: platform service state directory)")
reportIPFlag := fs.String("report-ip", envReportIP, "IP address to report (for multi-NIC systems)")
disableCephFlag := fs.Bool("disable-ceph", utils.ParseBool(envDisableCeph), "Disable local Ceph status polling")
showVersion := fs.Bool("version", false, "Print the agent version and exit")
@@ -1072,12 +1073,11 @@ func loadConfig(args []string, getenv func(string) string) (Config, error) {
// enrollment, use it instead of the bootstrap token embedded in the service
// config. This ensures the agent survives restarts after enrollment.
stateDir := strings.TrimSpace(*stateDirFlag)
if stateDir == "" {
stateDir = defaultAgentStateDir()
}
if *enrollFlag {
enrollStateDir := stateDir
if enrollStateDir == "" {
enrollStateDir = "/var/lib/pulse-agent"
}
runtimeTokenPath := filepath.Join(enrollStateDir, "runtime.token")
runtimeTokenPath := filepath.Join(stateDir, "runtime.token")
if content, err := os.ReadFile(runtimeTokenPath); err == nil {
if t := strings.TrimSpace(string(content)); t != "" {
token = t
@@ -1086,7 +1086,7 @@ func loadConfig(args []string, getenv func(string) string) (Config, error) {
}
if token == "" && *enrollFlag && !*selfTest {
return Config{}, fmt.Errorf("Pulse API token is required for enrollment (use --token, --token-file, PULSE_TOKEN env, or /var/lib/pulse-agent/token)")
return Config{}, fmt.Errorf("Pulse API token is required for enrollment (use --token, --token-file, PULSE_TOKEN env, or %s)", defaultTokenFilePath())
}
logLevel, err := parseLogLevel(*logLevelFlag)
@@ -1172,7 +1172,7 @@ func loadConfig(args []string, getenv func(string) string) (Config, error) {
KubeIncludeAllPods: *kubeIncludeAllPodsFlag,
KubeIncludeAllDeployments: *kubeIncludeAllDeploymentsFlag,
KubeMaxPods: kubeMaxPods,
StateDir: strings.TrimSpace(*stateDirFlag),
StateDir: stateDir,
DiskExclude: diskExclude,
ReportIP: strings.TrimSpace(*reportIPFlag),
DisableCeph: *disableCephFlag,
@@ -1294,13 +1294,30 @@ func resolveEnableCommands(enableFlag, disableFlag bool, envEnable, envDisable s
// 1. --token flag (direct value)
// 2. --token-file flag (read from file)
// 3. PULSE_TOKEN environment variable
// 4. Default token file at /var/lib/pulse-agent/token
// 4. Default token file under the platform state directory
//
// Reading from a file is more secure than CLI args as tokens won't appear in `ps` output.
func resolveToken(tokenFlag, tokenFileFlag, envToken string) string {
return resolveTokenInternal(tokenFlag, tokenFileFlag, envToken, os.ReadFile)
}
// defaultAgentStateDir mirrors where each platform's installer keeps agent
// state: %ProgramData%\Pulse on Windows (see scripts/install.ps1), the
// systemd/launchd convention /var/lib/pulse-agent everywhere else.
func defaultAgentStateDir() string {
if runtime.GOOS == "windows" {
if pd := strings.TrimSpace(os.Getenv("ProgramData")); pd != "" {
return filepath.Join(pd, "Pulse")
}
return `C:\ProgramData\Pulse`
}
return "/var/lib/pulse-agent"
}
func defaultTokenFilePath() string {
return filepath.Join(defaultAgentStateDir(), "token")
}
func resolveTokenInternal(tokenFlag, tokenFileFlag, envToken string, readFile func(string) ([]byte, error)) string {
// 1. Direct token from --token flag
if t := strings.TrimSpace(tokenFlag); t != "" {
@@ -1321,8 +1338,8 @@ func resolveTokenInternal(tokenFlag, tokenFileFlag, envToken string, readFile fu
return t
}
// 4. Default token file (most secure method for systemd services)
defaultTokenFile := "/var/lib/pulse-agent/token"
// 4. Default token file (most secure method for service installs)
defaultTokenFile := defaultTokenFilePath()
if content, err := readFile(defaultTokenFile); err == nil {
if t := strings.TrimSpace(string(content)); t != "" {
return t
+13
View File
@@ -921,6 +921,9 @@ func TestLoadConfig(t *testing.T) {
if cfg.EnableHost != true {
t.Errorf("expected host enabled by default")
}
if cfg.StateDir != defaultAgentStateDir() {
t.Errorf("expected platform state directory %q, got %q", defaultAgentStateDir(), cfg.StateDir)
}
})
t.Run("env overrides", func(t *testing.T) {
@@ -985,6 +988,16 @@ func TestLoadConfig(t *testing.T) {
}
})
t.Run("state directory flag overrides platform default", func(t *testing.T) {
cfg, err := loadConfig([]string{"-token", "test-token", "-state-dir", "/custom/pulse-state"}, func(s string) string { return "" })
if err != nil {
t.Fatal(err)
}
if cfg.StateDir != "/custom/pulse-state" {
t.Errorf("expected explicit state directory, got %q", cfg.StateDir)
}
})
t.Run("token optional when enrollment disabled", func(t *testing.T) {
cfg, err := loadConfig([]string{"-url", "http://token-optional.example.com", "-enable-host"}, func(s string) string { return "" })
if err != nil {
+2 -1
View File
@@ -947,7 +947,8 @@ Save-ConnectionState
# Build command line args (properly escaped)
$ServiceArgs = @(
"--url", "`"$Url`"",
"--interval", "`"$Interval`""
"--interval", "`"$Interval`"",
"--state-dir", "`"$StateDir`""
)
if (-not [string]::IsNullOrWhiteSpace($Token)) { $ServiceArgs += @("--token-file", "`"$TokenFilePath`"") }
if ($EnableHost) { $ServiceArgs += "--enable-host" } else { $ServiceArgs += "--enable-host=false" }
+1
View File
@@ -111,6 +111,7 @@ func TestInstallPS1OwnsWindowsServiceLoggingAndRecovery(t *testing.T) {
script := string(content)
required := []string{
`$ServiceArgs += @("--log-file", "` + "`" + `"$LogFile` + "`" + `"")`,
`"--state-dir", "` + "`" + `"$StateDir` + "`" + `"`,
`$scOutput = sc.exe failure $AgentName reset= 86400 actions= restart/5000/restart/5000/restart/5000`,
`Show-Error "Failed to configure service recovery actions: $scOutput"`,
`$scOutput = sc.exe failureflag $AgentName 1`,