mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
Preserve deployment mock settings on reload
Contract-Neutral: Preserve deployment-owned PULSE_MOCK environment values when the watched auth env file reloads; no public contract changes.
This commit is contained in:
+51
-12
@@ -43,6 +43,13 @@ type ConfigWatcher struct {
|
||||
onMockReload func() // Callback to trigger backend restart when PULSE_MOCK_* changes
|
||||
onAPITokenReload func() // Callback when API tokens are reloaded from disk
|
||||
pollInterval time.Duration
|
||||
mockEnvOwned map[string]struct{}
|
||||
mockEnvFallbacks map[string]mockEnvFallback
|
||||
}
|
||||
|
||||
type mockEnvFallback struct {
|
||||
value string
|
||||
present bool
|
||||
}
|
||||
|
||||
// NewConfigWatcher creates a new config watcher
|
||||
@@ -92,13 +99,15 @@ func NewConfigWatcher(config *Config) (*ConfigWatcher, error) {
|
||||
}
|
||||
|
||||
cw := &ConfigWatcher{
|
||||
config: config,
|
||||
envPath: envPath,
|
||||
apiTokensPath: apiTokensPath,
|
||||
persistence: persistence,
|
||||
watcher: watcher,
|
||||
stopChan: make(chan struct{}),
|
||||
pollInterval: 5 * time.Second,
|
||||
config: config,
|
||||
envPath: envPath,
|
||||
apiTokensPath: apiTokensPath,
|
||||
persistence: persistence,
|
||||
watcher: watcher,
|
||||
stopChan: make(chan struct{}),
|
||||
pollInterval: 5 * time.Second,
|
||||
mockEnvOwned: make(map[string]struct{}),
|
||||
mockEnvFallbacks: make(map[string]mockEnvFallback),
|
||||
}
|
||||
|
||||
// Get initial mod times and hash
|
||||
@@ -108,6 +117,11 @@ func NewConfigWatcher(config *Config) (*ConfigWatcher, error) {
|
||||
hash := sha256.Sum256(content)
|
||||
cw.lastEnvHash = hex.EncodeToString(hash[:])
|
||||
}
|
||||
if envMap, err := godotenv.Read(envPath); err == nil {
|
||||
for key := range extractMockEnv(envMap) {
|
||||
cw.mockEnvOwned[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
if stat, err := watcherOsStat(apiTokensPath); err == nil {
|
||||
cw.apiTokensLastModTime = stat.ModTime()
|
||||
@@ -416,7 +430,6 @@ func (cw *ConfigWatcher) reloadConfig() {
|
||||
// Update auth settings
|
||||
oldAuthUser := cw.config.AuthUser
|
||||
oldAuthPass := cw.config.AuthPass
|
||||
oldMockEnv := currentMockEnv()
|
||||
newMockEnv := extractMockEnv(envMap)
|
||||
|
||||
// Apply auth user
|
||||
@@ -449,7 +462,7 @@ func (cw *ConfigWatcher) reloadConfig() {
|
||||
}
|
||||
Mu.Unlock()
|
||||
|
||||
mockChanged := applyMockEnv(newMockEnv, oldMockEnv)
|
||||
mockChanged := cw.applyMockEnv(newMockEnv)
|
||||
if mockChanged {
|
||||
changes = append(changes, "mock runtime updated")
|
||||
}
|
||||
@@ -620,23 +633,49 @@ func extractMockEnv(envMap map[string]string) map[string]string {
|
||||
return mockEnv
|
||||
}
|
||||
|
||||
func applyMockEnv(next map[string]string, current map[string]string) bool {
|
||||
func (cw *ConfigWatcher) applyMockEnv(next map[string]string) bool {
|
||||
if cw.mockEnvOwned == nil {
|
||||
cw.mockEnvOwned = make(map[string]struct{})
|
||||
}
|
||||
if cw.mockEnvFallbacks == nil {
|
||||
cw.mockEnvFallbacks = make(map[string]mockEnvFallback)
|
||||
}
|
||||
|
||||
current := currentMockEnv()
|
||||
changed := false
|
||||
seen := make(map[string]struct{}, len(next))
|
||||
for key, value := range next {
|
||||
seen[key] = struct{}{}
|
||||
if _, owned := cw.mockEnvOwned[key]; !owned {
|
||||
fallbackValue, fallbackPresent := current[key]
|
||||
cw.mockEnvFallbacks[key] = mockEnvFallback{
|
||||
value: fallbackValue,
|
||||
present: fallbackPresent,
|
||||
}
|
||||
cw.mockEnvOwned[key] = struct{}{}
|
||||
}
|
||||
if currentValue, ok := current[key]; !ok || currentValue != value {
|
||||
_ = os.Setenv(key, value)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
for key := range current {
|
||||
for key := range cw.mockEnvOwned {
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
if err := os.Unsetenv(key); err == nil {
|
||||
fallback, hasFallback := cw.mockEnvFallbacks[key]
|
||||
currentValue, currentPresent := current[key]
|
||||
if hasFallback && fallback.present {
|
||||
if !currentPresent || currentValue != fallback.value {
|
||||
_ = os.Setenv(key, fallback.value)
|
||||
changed = true
|
||||
}
|
||||
} else if currentPresent {
|
||||
_ = os.Unsetenv(key)
|
||||
changed = true
|
||||
}
|
||||
delete(cw.mockEnvOwned, key)
|
||||
delete(cw.mockEnvFallbacks, key)
|
||||
}
|
||||
if changed {
|
||||
keys := make([]string, 0, len(next))
|
||||
|
||||
@@ -462,6 +462,15 @@ func TestConfigWatcher_ReloadConfig_RemovesMockSetting(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
envPath := filepath.Join(tempDir, ".env")
|
||||
t.Setenv("PULSE_AUTH_CONFIG_DIR", tempDir)
|
||||
previousMockValue, hadPreviousMockValue := os.LookupEnv("PULSE_MOCK_TEST")
|
||||
require.NoError(t, os.Unsetenv("PULSE_MOCK_TEST"))
|
||||
t.Cleanup(func() {
|
||||
if hadPreviousMockValue {
|
||||
_ = os.Setenv("PULSE_MOCK_TEST", previousMockValue)
|
||||
} else {
|
||||
_ = os.Unsetenv("PULSE_MOCK_TEST")
|
||||
}
|
||||
})
|
||||
|
||||
cfg := &Config{}
|
||||
cw, err := NewConfigWatcher(cfg)
|
||||
@@ -477,17 +486,34 @@ func TestConfigWatcher_ReloadConfig_RemovesMockSetting(t *testing.T) {
|
||||
os.Unsetenv("PULSE_MOCK_TEST")
|
||||
}
|
||||
|
||||
func TestConfigWatcher_ReloadConfig_MissingEnvRemovesMockSetting(t *testing.T) {
|
||||
func TestConfigWatcher_ReloadConfig_MissingEnvPreservesProcessMockSetting(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("PULSE_AUTH_CONFIG_DIR", tempDir)
|
||||
os.Setenv("PULSE_MOCK_TEST", "stale")
|
||||
t.Cleanup(func() { os.Unsetenv("PULSE_MOCK_TEST") })
|
||||
t.Setenv("PULSE_MOCK_TEST", "deployment-owned")
|
||||
|
||||
cw, err := NewConfigWatcher(&Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
cw.reloadConfig()
|
||||
assert.Equal(t, "", os.Getenv("PULSE_MOCK_TEST"))
|
||||
assert.Equal(t, "deployment-owned", os.Getenv("PULSE_MOCK_TEST"))
|
||||
}
|
||||
|
||||
func TestConfigWatcher_ReloadConfig_RestoresProcessMockSettingAfterFileOverride(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
envPath := filepath.Join(tempDir, ".env")
|
||||
t.Setenv("PULSE_AUTH_CONFIG_DIR", tempDir)
|
||||
t.Setenv("PULSE_MOCK_TEST", "deployment-owned")
|
||||
|
||||
cw, err := NewConfigWatcher(&Config{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, os.WriteFile(envPath, []byte(`PULSE_MOCK_TEST="file-owned"`), 0644))
|
||||
cw.reloadConfig()
|
||||
require.Equal(t, "file-owned", os.Getenv("PULSE_MOCK_TEST"))
|
||||
|
||||
require.NoError(t, os.WriteFile(envPath, []byte(""), 0644))
|
||||
cw.reloadConfig()
|
||||
assert.Equal(t, "deployment-owned", os.Getenv("PULSE_MOCK_TEST"))
|
||||
}
|
||||
|
||||
func TestConfigWatcher_ReloadAPITokens_Retries(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user