mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-26 13:39:47 +00:00
Preserve instance-wide settings during tenant recovery
The actual system-settings route deliberately owns instance-wide policy. Keep those effective values and process locks in the tenant runtime; restore only scoped nodes, tokens and storage ownership. This removes an unnecessary settings overlay and keeps the real scoped node consumer as the recovery proof. Contract-Neutral: Preserves existing instance-wide settings ownership while restoring the existing tenant resource, credential and storage isolation contract; no public schema, authority or lifecycle delta. Change-source: pulse-maintainer
This commit is contained in:
@@ -105,8 +105,8 @@ func TestConfigImportAppliesOnlySelectedTenant(t *testing.T) {
|
||||
if len(afterCfg.PVEInstances) != 1 || afterCfg.PVEInstances[0].Name != "restored" || afterCfg.PVEInstances[0].TokenValue != "synthetic-restored-node-secret" {
|
||||
t.Error("selected tenant runtime did not apply only its imported nodes/credentials")
|
||||
}
|
||||
if afterCfg.PublicURL != "https://restored.invalid" || afterCfg.PBSPollingInterval != 137*time.Second {
|
||||
t.Errorf("selected tenant runtime settings = URL %q, PBS %s; want imported values", afterCfg.PublicURL, afterCfg.PBSPollingInterval)
|
||||
if afterCfg.PublicURL != beforeDefault.PublicURL || afterCfg.PBSPollingInterval != beforeDefault.PBSPollingInterval {
|
||||
t.Errorf("selected tenant runtime settings = URL %q, PBS %s; want effective instance values", afterCfg.PublicURL, afterCfg.PBSPollingInterval)
|
||||
}
|
||||
if afterCfg.DataPath != persistences["tenant-a"].GetConfigDir() || afterCfg.ConfigPath != persistences["tenant-a"].GetConfigDir() {
|
||||
t.Error("selected tenant runtime adopted a non-target storage path")
|
||||
@@ -169,8 +169,8 @@ func TestConfigImportAppliesOnlySelectedTenant(t *testing.T) {
|
||||
if err := json.Unmarshal(settingsRec.Body.Bytes(), &settings); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if settings.PublicURL != "https://restored.invalid" || settings.PBSPollingInterval != 137 {
|
||||
t.Error("consuming settings handler did not read back the imported tenant values")
|
||||
if settings.PublicURL != beforeDefault.PublicURL || settings.PBSPollingInterval != int(beforeDefault.PBSPollingInterval.Seconds()) {
|
||||
t.Error("internal runtime settings no longer matched instance-wide policy")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,14 @@ package monitoring
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
)
|
||||
|
||||
// loadTenantRuntimeConfig deliberately does not call config.Load: that loader
|
||||
// owns the process environment, logger and default-org persistence. A tenant
|
||||
// starts (including after import) from its own persisted resources and settings,
|
||||
// retaining only the effective process settings and environment locks in base.
|
||||
// starts (including after import) from its own persisted resources,
|
||||
// retaining the effective instance settings and environment locks in base.
|
||||
func loadTenantRuntimeConfig(base *config.Config, p *config.ConfigPersistence) (*config.Config, error) {
|
||||
if base == nil || p == nil {
|
||||
return nil, fmt.Errorf("tenant configuration is unavailable")
|
||||
@@ -35,134 +34,10 @@ func loadTenantRuntimeConfig(base *config.Config, p *config.ConfigPersistence) (
|
||||
}
|
||||
cfg.APITokens = tokens
|
||||
cfg.SortAPITokens()
|
||||
s, err := p.LoadSystemSettings()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load tenant system settings: %w", err)
|
||||
}
|
||||
if s == nil {
|
||||
return cfg, nil
|
||||
}
|
||||
locked := func(keys ...string) bool {
|
||||
for _, key := range keys {
|
||||
if cfg.EnvOverrides[key] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if s.PVEPollingInterval > 0 && !locked("PVE_POLLING_INTERVAL") {
|
||||
cfg.PVEPollingInterval = min(time.Duration(s.PVEPollingInterval)*time.Second, time.Hour)
|
||||
}
|
||||
if s.PBSPollingInterval > 0 && !locked("PBS_POLLING_INTERVAL") {
|
||||
cfg.PBSPollingInterval = time.Duration(s.PBSPollingInterval) * time.Second
|
||||
}
|
||||
if s.PMGPollingInterval > 0 && !locked("PMG_POLLING_INTERVAL") {
|
||||
cfg.PMGPollingInterval = time.Duration(s.PMGPollingInterval) * time.Second
|
||||
}
|
||||
if s.BackupPollingInterval >= 0 && !locked("BACKUP_POLLING_INTERVAL") {
|
||||
cfg.BackupPollingInterval = time.Duration(s.BackupPollingInterval) * time.Second
|
||||
}
|
||||
if s.BackupPollingEnabled != nil && !locked("ENABLE_BACKUP_POLLING") {
|
||||
cfg.EnableBackupPolling = *s.BackupPollingEnabled
|
||||
}
|
||||
if s.AdaptivePollingEnabled != nil && !locked("ADAPTIVE_POLLING_ENABLED") {
|
||||
cfg.AdaptivePollingEnabled = *s.AdaptivePollingEnabled
|
||||
}
|
||||
if s.AdaptivePollingBaseInterval > 0 && !locked("ADAPTIVE_POLLING_BASE_INTERVAL") {
|
||||
cfg.AdaptivePollingBaseInterval = time.Duration(s.AdaptivePollingBaseInterval) * time.Second
|
||||
}
|
||||
if s.AdaptivePollingMinInterval > 0 && !locked("ADAPTIVE_POLLING_MIN_INTERVAL") {
|
||||
cfg.AdaptivePollingMinInterval = time.Duration(s.AdaptivePollingMinInterval) * time.Second
|
||||
}
|
||||
if s.AdaptivePollingMaxInterval > 0 && !locked("ADAPTIVE_POLLING_MAX_INTERVAL") {
|
||||
cfg.AdaptivePollingMaxInterval = time.Duration(s.AdaptivePollingMaxInterval) * time.Second
|
||||
}
|
||||
cfg.UpdateChannel = config.EffectiveUpdateChannel(s.UpdateChannel, cfg.UpdateChannel)
|
||||
cfg.AutoUpdateEnabled = config.EffectiveAutoUpdateEnabled(cfg.UpdateChannel, s.AutoUpdateEnabled)
|
||||
if !locked("ALLOWED_ORIGINS", "allowedOrigins") {
|
||||
cfg.AllowedOrigins = s.AllowedOrigins
|
||||
}
|
||||
if s.ConnectionTimeout > 0 && !locked("connectionTimeout") {
|
||||
cfg.ConnectionTimeout = time.Duration(s.ConnectionTimeout) * time.Second
|
||||
}
|
||||
if s.LogLevel != "" && !locked("logLevel") {
|
||||
cfg.LogLevel = s.LogLevel
|
||||
}
|
||||
if !locked("discoveryEnabled") {
|
||||
cfg.DiscoveryEnabled = s.DiscoveryEnabled
|
||||
}
|
||||
if !locked("discoverySubnet") {
|
||||
cfg.DiscoverySubnet = s.DiscoverySubnet
|
||||
}
|
||||
discovery := config.NormalizeDiscoveryConfig(config.CloneDiscoveryConfig(s.DiscoveryConfig))
|
||||
// Merge only persisted discovery fields not fixed by the process environment.
|
||||
if locked("discoveryEnvironmentOverride") {
|
||||
discovery.EnvironmentOverride = cfg.Discovery.EnvironmentOverride
|
||||
}
|
||||
if locked("discoverySubnetAllowlist") {
|
||||
discovery.SubnetAllowlist = cfg.Discovery.SubnetAllowlist
|
||||
}
|
||||
if locked("discoverySubnetBlocklist") {
|
||||
discovery.SubnetBlocklist = cfg.Discovery.SubnetBlocklist
|
||||
}
|
||||
if locked("discoveryMaxHostsPerScan") {
|
||||
discovery.MaxHostsPerScan = cfg.Discovery.MaxHostsPerScan
|
||||
}
|
||||
if locked("discoveryMaxConcurrent") {
|
||||
discovery.MaxConcurrent = cfg.Discovery.MaxConcurrent
|
||||
}
|
||||
if locked("discoveryEnableReverseDns") {
|
||||
discovery.EnableReverseDNS = cfg.Discovery.EnableReverseDNS
|
||||
}
|
||||
if locked("discoveryScanGateways") {
|
||||
discovery.ScanGateways = cfg.Discovery.ScanGateways
|
||||
}
|
||||
if locked("discoveryDialTimeoutMs") {
|
||||
discovery.DialTimeout = cfg.Discovery.DialTimeout
|
||||
}
|
||||
if locked("discoveryHttpTimeoutMs") {
|
||||
discovery.HTTPTimeout = cfg.Discovery.HTTPTimeout
|
||||
}
|
||||
cfg.Discovery = discovery
|
||||
if !locked("temperatureMonitoringEnabled") {
|
||||
cfg.TemperatureMonitoringEnabled = s.TemperatureMonitoringEnabled
|
||||
}
|
||||
if s.DNSCacheTimeout > 0 && !locked("DNS_CACHE_TIMEOUT") {
|
||||
cfg.DNSCacheTimeout = time.Duration(s.DNSCacheTimeout) * time.Second
|
||||
}
|
||||
if !locked("SSH_PORT") {
|
||||
cfg.SSHPort = s.SSHPort
|
||||
if cfg.SSHPort <= 0 {
|
||||
cfg.SSHPort = 22
|
||||
}
|
||||
}
|
||||
if !locked("PULSE_AUTH_HIDE_LOCAL_LOGIN", "hideLocalLogin") {
|
||||
cfg.HideLocalLogin = s.HideLocalLogin
|
||||
}
|
||||
if !locked("PULSE_DISABLE_DOCKER_UPDATE_ACTIONS", "disableDockerUpdateActions") {
|
||||
cfg.DisableDockerUpdateActions = s.DisableDockerUpdateActions
|
||||
}
|
||||
if !locked("PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY", "enableProxmoxGuestDockerInventory") {
|
||||
cfg.EnableProxmoxGuestDockerInventory = s.EnableProxmoxGuestDockerInventory
|
||||
}
|
||||
if !locked("PULSE_TELEMETRY", "telemetryEnabled") {
|
||||
cfg.TelemetryEnabled = s.TelemetryEnabled == nil || *s.TelemetryEnabled
|
||||
}
|
||||
if !locked("publicURL") {
|
||||
cfg.PublicURL = s.PublicURL
|
||||
}
|
||||
if s.MetricsRetentionRawHours > 0 {
|
||||
cfg.MetricsRetentionRawHours = s.MetricsRetentionRawHours
|
||||
}
|
||||
if s.MetricsRetentionMinuteHours > 0 {
|
||||
cfg.MetricsRetentionMinuteHours = s.MetricsRetentionMinuteHours
|
||||
}
|
||||
if s.MetricsRetentionHourlyDays > 0 {
|
||||
cfg.MetricsRetentionHourlyDays = s.MetricsRetentionHourlyDays
|
||||
}
|
||||
if s.MetricsRetentionDailyDays > 0 {
|
||||
cfg.MetricsRetentionDailyDays = s.MetricsRetentionDailyDays
|
||||
}
|
||||
// System settings remain instance-wide: their existing API updates the
|
||||
// base config and all live tenant monitors. Importing a tenant archive
|
||||
// must not invent per-tenant overrides or re-read process .env/logging.
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
)
|
||||
|
||||
func TestLoadTenantRuntimeConfigUsesScopedResourcesAndPreservesEnvironment(t *testing.T) {
|
||||
func TestLoadTenantRuntimeConfigUsesScopedResourcesAndPreservesInstanceSettings(t *testing.T) {
|
||||
p := config.NewConfigPersistence(t.TempDir())
|
||||
settings := config.DefaultSystemSettings()
|
||||
settings.PublicURL = "https://tenant.invalid"
|
||||
@@ -59,14 +59,14 @@ func TestLoadTenantRuntimeConfigUsesScopedResourcesAndPreservesEnvironment(t *te
|
||||
if len(cfg.APITokens) != 1 || cfg.APITokens[0].ID != "tenant-token" || cfg.APIToken != "synthetic-tenant-hash" {
|
||||
t.Error("tenant API tokens did not replace inherited tokens and legacy alias")
|
||||
}
|
||||
if cfg.PublicURL != settings.PublicURL || cfg.AllowedOrigins != settings.AllowedOrigins || cfg.PBSPollingInterval != 137*time.Second || !cfg.DisableDockerUpdateActions {
|
||||
t.Error("saved tenant settings were not applied")
|
||||
if cfg.PublicURL != base.PublicURL || cfg.AllowedOrigins != base.AllowedOrigins || cfg.PBSPollingInterval != base.PBSPollingInterval || cfg.DisableDockerUpdateActions != base.DisableDockerUpdateActions {
|
||||
t.Error("tenant archive settings overrode instance-wide runtime policy")
|
||||
}
|
||||
if cfg.PVEPollingInterval != 42*time.Second || !cfg.HideLocalLogin || cfg.Discovery.MaxConcurrent != 11 {
|
||||
t.Error("tenant settings bypassed effective process environment locks")
|
||||
}
|
||||
if !reflect.DeepEqual(cfg.Discovery.SubnetAllowlist, settings.DiscoveryConfig.SubnetAllowlist) {
|
||||
t.Error("unlocked discovery setting was not applied")
|
||||
if !reflect.DeepEqual(cfg.Discovery, base.Discovery) {
|
||||
t.Error("tenant archive settings overrode instance-wide discovery policy")
|
||||
}
|
||||
if cfg.AuthUser != base.AuthUser || cfg.AuthPass != base.AuthPass || cfg.ProxyAuthSecret != base.ProxyAuthSecret || cfg.TLSCertFile != base.TLSCertFile || cfg.TLSKeyFile != base.TLSKeyFile || cfg.FrontendPort != base.FrontendPort {
|
||||
t.Error("tenant loader changed process identity or listener settings")
|
||||
@@ -84,7 +84,7 @@ func TestLoadTenantRuntimeConfigUsesScopedResourcesAndPreservesEnvironment(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadTenantRuntimeConfigEmptyInventoryAndCorruptSettings(t *testing.T) {
|
||||
func TestLoadTenantRuntimeConfigEmptyInventoryDoesNotInheritCredentials(t *testing.T) {
|
||||
p := config.NewConfigPersistence(t.TempDir())
|
||||
base := &config.Config{PVEInstances: []config.PVEInstance{{Name: "default"}}, APITokens: []config.APITokenRecord{{ID: "default", Hash: "synthetic-hash"}}, APIToken: "synthetic-hash"}
|
||||
cfg, err := loadTenantRuntimeConfig(base, p)
|
||||
@@ -97,8 +97,8 @@ func TestLoadTenantRuntimeConfigEmptyInventoryAndCorruptSettings(t *testing.T) {
|
||||
if err := os.WriteFile(filepath.Join(p.GetConfigDir(), "system.json"), []byte("{invalid"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := loadTenantRuntimeConfig(base, p); err == nil {
|
||||
t.Error("unreadable tenant settings fell back to the default config")
|
||||
if _, err := loadTenantRuntimeConfig(base, p); err != nil {
|
||||
t.Errorf("tenant-local system record incorrectly became instance policy: %v", err)
|
||||
}
|
||||
if _, err := loadTenantRuntimeConfig(nil, p); err == nil {
|
||||
t.Error("missing base config was accepted")
|
||||
|
||||
Reference in New Issue
Block a user