diff --git a/internal/api/config_import_tenant_runtime_test.go b/internal/api/config_import_tenant_runtime_test.go index d60bcc6b7f..8c4cb324fe 100644 --- a/internal/api/config_import_tenant_runtime_test.go +++ b/internal/api/config_import_tenant_runtime_test.go @@ -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") } } diff --git a/internal/monitoring/tenant_runtime_config.go b/internal/monitoring/tenant_runtime_config.go index 3d33c5124d..beb4a284e8 100644 --- a/internal/monitoring/tenant_runtime_config.go +++ b/internal/monitoring/tenant_runtime_config.go @@ -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 } diff --git a/internal/monitoring/tenant_runtime_config_test.go b/internal/monitoring/tenant_runtime_config_test.go index dfe406418c..a14357f18e 100644 --- a/internal/monitoring/tenant_runtime_config_test.go +++ b/internal/monitoring/tenant_runtime_config_test.go @@ -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")