diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 0ab492cf56..b3e277a8d5 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -6690,6 +6690,17 @@ cleared rather than read as already-consumed, so a fresh token minted after removal always re-enrolls the host instead of rejecting its reports indefinitely with 400s that no reinstall can escape (#1772). +### Remote configuration refreshes are transition-quiet + +The Unified Agent polls its authoritative remote configuration every minute, +but an unchanged response is reconciliation rather than an operator event. +Repeated values for the host report interval, report-IP override, Ceph +collection setting, availability assignments, and command-execution state +must not reset schedules, restart command clients, or emit info-level journal +entries. Actual state transitions and malformed or failed refreshes remain +visible at their existing levels. This keeps managed agents quiet during +healthy steady-state operation without hiding configuration changes or faults. + ### Agent token issuance and revocation remain continuous across persistence failure An operator-created API token carrying agent authority, including the shared diff --git a/internal/hostagent/agent.go b/internal/hostagent/agent.go index 9821c75340..0d5f7f388d 100644 --- a/internal/hostagent/agent.go +++ b/internal/hostagent/agent.go @@ -1444,28 +1444,40 @@ func (a *Agent) ApplyRemoteConfig(settings map[string]interface{}, commandsEnabl a.configMu.Lock() if a.interval != interval { intervalChanged = true + a.interval = interval + a.cfg.Interval = interval } - a.interval = interval - a.cfg.Interval = interval a.configMu.Unlock() if intervalChanged { a.signalRemoteConfigChanged() + a.logger.Info().Dur("interval", interval).Msg("Applied remote host report interval") } - a.logger.Info().Dur("interval", interval).Msg("Applied remote host report interval") } if reportIP, ok := remoteStringSetting(settings, "report_ip"); ok { trimmedReportIP := strings.TrimSpace(reportIP) + reportIPChanged := false a.configMu.Lock() - a.reportIP = trimmedReportIP - a.cfg.ReportIP = trimmedReportIP + if a.reportIP != trimmedReportIP { + reportIPChanged = true + a.reportIP = trimmedReportIP + a.cfg.ReportIP = trimmedReportIP + } a.configMu.Unlock() - a.logger.Info().Str("report_ip", trimmedReportIP).Msg("Applied remote host report IP override") + if reportIPChanged { + a.logger.Info().Str("report_ip", trimmedReportIP).Msg("Applied remote host report IP override") + } } if disableCeph, ok := remoteBoolSetting(settings, "disable_ceph"); ok { + disableCephChanged := false a.configMu.Lock() - a.cfg.DisableCeph = disableCeph + if a.cfg.DisableCeph != disableCeph { + disableCephChanged = true + a.cfg.DisableCeph = disableCeph + } a.configMu.Unlock() - a.logger.Info().Bool("disable_ceph", disableCeph).Msg("Applied remote Ceph collection setting") + if disableCephChanged { + a.logger.Info().Bool("disable_ceph", disableCeph).Msg("Applied remote Ceph collection setting") + } } a.applyRemoteAvailabilityTargets(settings) if remoteConfigAppliedWithoutRestart(settings) && remoteconfig.HasAppliedDesiredConfig(commandsEnabled, settings) { @@ -1528,8 +1540,6 @@ func (a *Agent) applyRemoteConfig(commandsEnabled bool) { case commandsEnabled && !currentlyEnabled: clientToStart = a.newCommandClient(commandCfg, a.agentID, a.hostname, a.platform, a.agentVersion) a.commandClient = clientToStart - case commandsEnabled && currentlyEnabled: - clientToStart = a.commandClient case !commandsEnabled && currentlyEnabled: shouldStop = true } diff --git a/internal/hostagent/agent_new_test.go b/internal/hostagent/agent_new_test.go index 00f619d262..291c00be25 100644 --- a/internal/hostagent/agent_new_test.go +++ b/internal/hostagent/agent_new_test.go @@ -1,6 +1,7 @@ package hostagent import ( + "bytes" "context" "crypto/tls" "errors" @@ -572,6 +573,43 @@ func TestAgentApplyRemoteConfigKeepsPriorFingerprintWhenRestartIsRequired(t *tes } } +func TestAgentApplyRemoteConfigDoesNotLogUnchangedSettings(t *testing.T) { + var logs bytes.Buffer + logger := zerolog.New(&logs).Level(zerolog.InfoLevel) + commandClient := &CommandClient{} + commandsEnabled := true + agent := &Agent{ + cfg: Config{ + EnableCommands: true, + Interval: 30 * time.Second, + ReportIP: "192.0.2.20", + DisableCeph: true, + }, + interval: 30 * time.Second, + reportIP: "192.0.2.20", + remoteConfigChanged: make(chan struct{}, 1), + logger: logger, + commandClient: commandClient, + } + settings := map[string]interface{}{ + "interval": "30s", + "report_ip": "192.0.2.20", + "disable_ceph": true, + } + + agent.ApplyRemoteConfig(settings, &commandsEnabled) + agent.ApplyRemoteConfig(settings, &commandsEnabled) + + if logs.Len() != 0 { + t.Fatalf("unchanged remote config emitted info logs: %s", logs.String()) + } + select { + case <-agent.remoteConfigChanged: + t.Fatal("unchanged interval signaled a report-loop reset") + default: + } +} + func TestNewRejectsInvalidDeploySSHUser(t *testing.T) { mc := &mockCollector{ hostInfoFn: func(context.Context) (*gohost.InfoStat, error) {