304 lines
8.0 KiB
Go
304 lines
8.0 KiB
Go
package orchestration
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/cloudflare/cloudflared/config"
|
|
"github.com/cloudflare/cloudflared/ingress"
|
|
)
|
|
|
|
func TestNewLocalConfigWatcher(t *testing.T) {
|
|
log := zerolog.Nop()
|
|
orchestrator := createTestOrchestrator(t)
|
|
|
|
watcher := NewLocalConfigWatcher(orchestrator, "/tmp/config.yaml", &log)
|
|
require.NotNil(t, watcher)
|
|
require.Equal(t, "/tmp/config.yaml", watcher.configPath)
|
|
require.Equal(t, int32(localConfigVersionStart), watcher.Version())
|
|
}
|
|
|
|
func TestLocalConfigWatcher_ReloadConfig(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "config.yaml")
|
|
|
|
configContent := `
|
|
tunnel: test-tunnel-id
|
|
ingress:
|
|
- hostname: example.com
|
|
service: http://localhost:8080
|
|
- service: http_status:404
|
|
`
|
|
err := os.WriteFile(configPath, []byte(configContent), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
log := zerolog.Nop()
|
|
orchestrator := createTestOrchestrator(t)
|
|
|
|
watcher := NewLocalConfigWatcher(orchestrator, configPath, &log)
|
|
|
|
watcher.ReloadConfig()
|
|
|
|
require.Equal(t, int32(localConfigVersionStart+1), watcher.Version())
|
|
}
|
|
|
|
func TestLocalConfigWatcher_ReloadConfig_InvalidYAML(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "config.yaml")
|
|
|
|
err := os.WriteFile(configPath, []byte("invalid: yaml: ["), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
log := zerolog.Nop()
|
|
orchestrator := createTestOrchestrator(t)
|
|
|
|
watcher := NewLocalConfigWatcher(orchestrator, configPath, &log)
|
|
|
|
watcher.ReloadConfig()
|
|
|
|
require.Equal(t, int32(localConfigVersionStart), watcher.Version())
|
|
}
|
|
|
|
func TestLocalConfigWatcher_ReloadConfig_InvalidIngress(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "config.yaml")
|
|
|
|
// Missing catch-all rule (no empty hostname at end)
|
|
configContent := `
|
|
tunnel: test-tunnel-id
|
|
ingress:
|
|
- hostname: example.com
|
|
service: http://localhost:8080
|
|
`
|
|
err := os.WriteFile(configPath, []byte(configContent), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
log := zerolog.Nop()
|
|
orchestrator := createTestOrchestrator(t)
|
|
|
|
watcher := NewLocalConfigWatcher(orchestrator, configPath, &log)
|
|
|
|
watcher.ReloadConfig()
|
|
|
|
require.Equal(t, int32(localConfigVersionStart), watcher.Version())
|
|
}
|
|
|
|
func TestLocalConfigWatcher_WatcherItemDidChange(t *testing.T) {
|
|
log := zerolog.Nop()
|
|
orchestrator := createTestOrchestrator(t)
|
|
|
|
watcher := NewLocalConfigWatcher(orchestrator, "/tmp/config.yaml", &log)
|
|
|
|
watcher.WatcherItemDidChange("/tmp/config.yaml")
|
|
|
|
select {
|
|
case <-watcher.reloadChan:
|
|
default:
|
|
t.Fatal("Expected reload channel to receive signal")
|
|
}
|
|
}
|
|
|
|
func TestLocalConfigWatcher_WatcherItemDidChange_NonBlocking(t *testing.T) {
|
|
log := zerolog.Nop()
|
|
orchestrator := createTestOrchestrator(t)
|
|
|
|
watcher := NewLocalConfigWatcher(orchestrator, "/tmp/config.yaml", &log)
|
|
|
|
watcher.reloadChan <- struct{}{}
|
|
|
|
watcher.WatcherItemDidChange("/tmp/config.yaml")
|
|
|
|
select {
|
|
case <-watcher.reloadChan:
|
|
default:
|
|
t.Fatal("Expected reload channel to have signal")
|
|
}
|
|
}
|
|
|
|
func TestLocalConfigWatcher_Run_ManualReload(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "config.yaml")
|
|
|
|
configContent := `
|
|
tunnel: test-tunnel-id
|
|
ingress:
|
|
- hostname: example.com
|
|
service: http://localhost:8080
|
|
- service: http_status:404
|
|
`
|
|
err := os.WriteFile(configPath, []byte(configContent), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
log := zerolog.Nop()
|
|
orchestrator := createTestOrchestrator(t)
|
|
|
|
watcher := NewLocalConfigWatcher(orchestrator, configPath, &log)
|
|
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
reloadC := make(chan struct{}, 1)
|
|
|
|
readyC := watcher.Run(ctx, reloadC)
|
|
<-readyC // Wait until watcher is ready
|
|
|
|
// Send reload signal
|
|
reloadC <- struct{}{}
|
|
|
|
// Wait for version to increment
|
|
require.Eventually(t, func() bool {
|
|
return watcher.Version() >= localConfigVersionStart+1
|
|
}, 2*time.Second, 10*time.Millisecond, "version should be incremented after reload")
|
|
}
|
|
|
|
func TestLocalConfigWatcher_Run_FileChange(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "config.yaml")
|
|
|
|
configContent := `
|
|
tunnel: test-tunnel-id
|
|
ingress:
|
|
- hostname: example.com
|
|
service: http://localhost:8080
|
|
- service: http_status:404
|
|
`
|
|
err := os.WriteFile(configPath, []byte(configContent), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
log := zerolog.Nop()
|
|
orchestrator := createTestOrchestrator(t)
|
|
|
|
watcher := NewLocalConfigWatcher(orchestrator, configPath, &log)
|
|
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
reloadC := make(chan struct{}, 1)
|
|
|
|
readyC := watcher.Run(ctx, reloadC)
|
|
<-readyC // Wait until watcher is ready
|
|
|
|
newConfigContent := `
|
|
tunnel: test-tunnel-id
|
|
ingress:
|
|
- hostname: new-example.com
|
|
service: http://localhost:9090
|
|
- service: http_status:404
|
|
`
|
|
// Write the config file. We may need to write multiple times because fsnotify
|
|
// may not have started watching yet. We write with increasing delays to allow
|
|
// the debounce timer (500ms) to fire between writes.
|
|
written := false
|
|
for range 5 {
|
|
err = os.WriteFile(configPath, []byte(newConfigContent), 0o600)
|
|
require.NoError(t, err)
|
|
written = true
|
|
// Wait longer than debounce interval to allow reload to happen
|
|
time.Sleep(600 * time.Millisecond)
|
|
if watcher.Version() >= localConfigVersionStart+1 {
|
|
break
|
|
}
|
|
}
|
|
require.True(t, written, "should have written config file")
|
|
require.GreaterOrEqual(t, watcher.Version(), int32(localConfigVersionStart+1), "version should be incremented after file change")
|
|
}
|
|
|
|
func TestLocalConfigWatcher_ConcurrentReloads(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "config.yaml")
|
|
|
|
configContent := `
|
|
tunnel: test-tunnel-id
|
|
ingress:
|
|
- hostname: example.com
|
|
service: http://localhost:8080
|
|
- service: http_status:404
|
|
`
|
|
err := os.WriteFile(configPath, []byte(configContent), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
log := zerolog.Nop()
|
|
orchestrator := createTestOrchestrator(t)
|
|
|
|
watcher := NewLocalConfigWatcher(orchestrator, configPath, &log)
|
|
|
|
// Run multiple concurrent reloads
|
|
const numGoroutines = 10
|
|
var wg sync.WaitGroup
|
|
wg.Add(numGoroutines)
|
|
|
|
for range numGoroutines {
|
|
go func() {
|
|
defer wg.Done()
|
|
watcher.ReloadConfig()
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// With TryLock, concurrent reloads are skipped if one is already in progress.
|
|
// At least one reload should succeed (version >= start+1).
|
|
// Due to TryLock skipping, version likely won't reach start+numGoroutines.
|
|
finalVersion := watcher.Version()
|
|
require.GreaterOrEqual(t, finalVersion, int32(localConfigVersionStart+1),
|
|
"At least one reload should have succeeded")
|
|
require.LessOrEqual(t, finalVersion, int32(localConfigVersionStart+numGoroutines),
|
|
"Version should not exceed expected reloads")
|
|
}
|
|
|
|
func TestLocalConfigWatcher_Run_ContextCancellation(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
configPath := filepath.Join(tempDir, "config.yaml")
|
|
|
|
configContent := `
|
|
tunnel: test-tunnel-id
|
|
ingress:
|
|
- service: http_status:404
|
|
`
|
|
err := os.WriteFile(configPath, []byte(configContent), 0o600)
|
|
require.NoError(t, err)
|
|
|
|
log := zerolog.Nop()
|
|
orchestrator := createTestOrchestrator(t)
|
|
watcher := NewLocalConfigWatcher(orchestrator, configPath, &log)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
reloadC := make(chan struct{}, 1)
|
|
|
|
readyC := watcher.Run(ctx, reloadC)
|
|
<-readyC
|
|
|
|
// Cancel context and verify watcher stops without panic or hang
|
|
cancel()
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
|
|
func createTestOrchestrator(t *testing.T) *Orchestrator {
|
|
t.Helper()
|
|
|
|
log := zerolog.Nop()
|
|
originDialer := ingress.NewOriginDialer(ingress.OriginConfig{
|
|
DefaultDialer: ingress.NewDialer(ingress.WarpRoutingConfig{
|
|
ConnectTimeout: config.CustomDuration{Duration: 1 * time.Second},
|
|
TCPKeepAlive: config.CustomDuration{Duration: 15 * time.Second},
|
|
MaxActiveFlows: 0,
|
|
}),
|
|
TCPWriteTimeout: 1 * time.Second,
|
|
}, &log)
|
|
|
|
initConfig := &Config{
|
|
Ingress: &ingress.Ingress{},
|
|
OriginDialerService: originDialer,
|
|
}
|
|
|
|
orchestrator, err := NewOrchestrator(t.Context(), initConfig, nil, []ingress.Rule{}, &log)
|
|
require.NoError(t, err)
|
|
|
|
return orchestrator
|
|
}
|