package config import ( "context" "fmt" "os" "path/filepath" "strings" "syscall" "testing" "time" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/signal" "github.com/pomerium/pomerium/internal/testutil" ) func TestFileWatcherSource(t *testing.T) { t.Parallel() tmpdir := t.TempDir() err := os.WriteFile(filepath.Join(tmpdir, "example.txt"), []byte{1}, 0o600) if !assert.NoError(t, err) { return } err = os.WriteFile(filepath.Join(tmpdir, "kubernetes-example.txt"), []byte{2}, 0o600) if !assert.NoError(t, err) { return } newTest := func(enabled bool) func(*testing.T) { return func(t *testing.T) { ssrc := NewStaticSource(&Config{ Options: &Options{ CAFile: filepath.Join(tmpdir, "example.txt"), Policies: []Policy{{ KubernetesServiceAccountTokenFile: filepath.Join(tmpdir, "kubernetes-example.txt"), }}, RuntimeFlags: map[RuntimeFlag]bool{ RuntimeFlagConfigHotReload: enabled, }, }, }) src := NewFileWatcherSource(context.Background(), ssrc) ch := make(chan struct{}, 10) src.OnConfigChange(context.Background(), func(_ context.Context, _ *Config) { ch <- struct{}{} }) err := os.WriteFile(filepath.Join(tmpdir, "example.txt"), []byte{1, 2}, 0o600) if !assert.NoError(t, err) { return } select { case <-ch: if !enabled { t.Error("expected OnConfigChange not to be fired after modifying a file") } case <-time.After(time.Second): if enabled { t.Error("expected OnConfigChange to be fired after modifying a file") } } require.Empty(t, ch, "expected exactly one OnConfigChange event") err = os.WriteFile(filepath.Join(tmpdir, "kubernetes-example.txt"), []byte{2, 3}, 0o600) if !assert.NoError(t, err) { return } select { case <-ch: if !enabled { t.Error("expected OnConfigChange not to be fired after modifying a file") } case <-time.After(time.Second): if enabled { t.Error("expected OnConfigChange to be fired after modifying a policy file") } } require.Empty(t, ch, "expected exactly one OnConfigChange event") ssrc.SetConfig(context.Background(), &Config{ Options: &Options{ CAFile: filepath.Join(tmpdir, "example.txt"), }, }) select { case <-ch: case <-time.After(time.Second): if enabled { t.Error("expected OnConfigChange to be fired after triggering a change to the underlying source") } } require.Empty(t, ch, "expected exactly one OnConfigChange event") } } t.Run("Hot Reload Enabled", newTest(true)) t.Run("Hot Reload Disabled", newTest(false)) } func TestFileOrEnvironmentSource(t *testing.T) { t.Parallel() tmpdir := t.TempDir() err := os.WriteFile(filepath.Join(tmpdir, "example.txt"), []byte{1}, 0o600) if !assert.NoError(t, err) { return } err = os.WriteFile(filepath.Join(tmpdir, "kubernetes-example.txt"), []byte{2}, 0o600) if !assert.NoError(t, err) { return } newTest := func(enabled bool) func(*testing.T) { return func(t *testing.T) { initialConfigYaml := fmt.Sprintf(` certificate_authority_file: %s policy: - from: https://foo to: https://bar kubernetes_service_account_token_file: %s codec_type: auto runtime_flags: config_hot_reload: %t `, filepath.Join(tmpdir, "example.txt"), filepath.Join(tmpdir, "kubernetes-example.txt"), enabled, ) configFilePath := filepath.Join(tmpdir, "config.yaml") err := os.WriteFile(configFilePath, []byte(initialConfigYaml), 0o600) require.NoError(t, err) var src Source src, err = NewFileOrEnvironmentSource(context.Background(), configFilePath, "") require.NoError(t, err) src = NewFileWatcherSource(context.Background(), src) ch := make(chan struct{}, 10) src.OnConfigChange(context.Background(), func(_ context.Context, _ *Config) { ch <- struct{}{} }) err = os.WriteFile(filepath.Join(tmpdir, "example.txt"), []byte{1, 2}, 0o600) require.NoError(t, err) select { case <-ch: if !enabled { t.Error("expected OnConfigChange not to be fired after modifying a file") } case <-time.After(time.Second): if enabled { t.Error("expected OnConfigChange to be fired after modifying a file") } } require.Empty(t, ch, "expected exactly one OnConfigChange event") err = os.WriteFile(filepath.Join(tmpdir, "kubernetes-example.txt"), []byte{2, 3}, 0o600) if !assert.NoError(t, err) { return } select { case <-ch: if !enabled { t.Error("expected OnConfigChange not to be fired after modifying a file") } case <-time.After(time.Second): if enabled { t.Error("expected OnConfigChange to be fired after modifying a policy file") } } require.Empty(t, ch, "expected exactly one OnConfigChange event") } } t.Run("Hot Reload Enabled", newTest(true)) t.Run("Hot Reload Disabled", newTest(false)) t.Run("SIGHUP", func(t *testing.T) { t.Parallel() ready := signal.New() readyCh := ready.Bind() ctx := testutil.GetContext(t, time.Minute) ctx = log.Ctx(ctx).Hook(zerolog.HookFunc(func(_ *zerolog.Event, _ zerolog.Level, message string) { if strings.Contains(message, "received SIGHUP") { ready.Broadcast(ctx) } })).WithContext(ctx) tmp := t.TempDir() cfgFP := filepath.Join(tmp, "config.json") require.NoError(t, os.WriteFile(cfgFP, []byte(`{}`), 0o600)) _, err := NewFileOrEnvironmentSource(ctx, cfgFP, "") assert.NoError(t, err) require.NoError(t, syscall.Kill(syscall.Getpid(), syscall.SIGHUP)) select { case <-ctx.Done(): t.Error("expected to receive SIGHUP log message") case <-readyCh: } }) }